feat: Initial Claude skills for websimple-devops

This commit is contained in:
2026-05-26 10:42:20 -04:00
commit a37c19bb87
16 changed files with 2228 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.bak
*.bak.*

102
SKILL.md Normal file
View File

@@ -0,0 +1,102 @@
---
name: websimple-devops
description: Operate the Websimple WordPress development environment — the local websimple-stack Docker Compose project, WordPress site lifecycle (create, sync, reset), Composer dependencies, Kaliroots child themes, ACF conventions, asset build pipelines, browser/Xdebug debugging, and Gitea metadata lookups. Use whenever the user mentions a Websimple WordPress site, a `wp-sites` Gitea repository, the `ledevsimple.ca` local domain (or another `WEBSIMPLE_STACK_DOMAIN`), `websimple-stack`, `wp-apache2`, Kaliroots, ACF in a Websimple context, or any local WordPress dev task on a developer machine configured for Websimple. Also use when a request implies operations on a Websimple project even without naming it explicitly — for example "spin up a local copy of example.com", "pull production into local for site X", "fix the Composer install for this site", "what's the production URL for client Y".
---
# Websimple DevOps
Operate Websimple's local WordPress development environment: the `websimple-stack` Docker Compose project, the WordPress sites it serves, the Composer-managed plugins/themes inside them, and the Gitea metadata that holds deployment values.
This skill packages a router (this file) plus 13 topic-specific reference files under `references/` and a Bash wrapper for Gitea API reads under `scripts/`. The reference files contain the operational detail — read them when a task lands in their area. Don't try to remember their content; resolve the topic, then read the file.
## Required environment variables
These must be set in the user's shell before any operation that needs them. Fail fast and ask the user to export them if they're missing — do not guess defaults.
| Variable | Meaning |
|---|---|
| `WEBSIMPLE_STACK_PATH` | Absolute path to the local `websimple-stack` Docker Compose checkout. |
| `WEBSIMPLE_STACK_PROTOCOL` | `http` or `https` — protocol used by local Traefik routing. |
| `WEBSIMPLE_STACK_DOMAIN` | Local domain suffix Traefik routes wildcard subdomains for (e.g. `ledevsimple.ca`). |
| `WP_LOCAL_ROOT_PATH` | Absolute path to the shared WordPress root path served by the `wp-apache2` service. |
| `WEBSIMPLE_GITEA_API_TOKEN` | Read-only Gitea API token for metadata lookups. |
Treat all of these as sensitive in the sense that the token must never be echoed back in chat. The paths and domain are not secrets, but still read them from the environment rather than hard-coding so the skill stays portable across user setups.
### Loading the variables (Cowork sessions)
The Cowork bash sandbox does **not** inherit the user's host `.bashrc`/`.profile` — each bash call starts fresh and only sees env vars injected by the harness. The user keeps a shared env file at `~/.config/cowork/env.sh` on the host (also sourced by their host `.bashrc`, so it's a single source of truth). In Cowork, that path appears under the session's mount as `/sessions/<session-id>/mnt/cowork/env.sh`.
At the start of any bash call that needs these variables, source the file if it exists. Resolve the session mount path dynamically rather than hard-coding a session ID:
```bash
COWORK_ENV="$(ls /sessions/*/mnt/cowork/env.sh 2>/dev/null | head -1)"
[ -n "$COWORK_ENV" ] && . "$COWORK_ENV"
```
If `env.sh` isn't found, fall back to normal behavior: check each variable and, when something's missing, ask the user either to export it or to approve mounting `~/.config/cowork/` (whichever is the actual gap).
## Reference index — read the file that matches the task
For any non-trivial task, **read the matching reference file first** before producing commands or making changes. The reference files encode conventions that aren't obvious from the codebase and that the user expects to be followed.
| Concern | Reference file |
|---|---|
| Host environment, Docker Compose, Traefik, required executables, MySQL/WP-CLI defaults | `references/websimple-stack.md` |
| Websimple Gitea conventions, repository URLs, Actions variables, token usage | `references/websimple-gitea.md` |
| WordPress project naming, slug derivation, local/remote URL/DB/path conventions | `references/wp-project.md` |
| Local site lifecycle — create, clone, provision from remote, reset, delete | `references/wp-local-site.md` |
| Composer dependency management, Satis private repo, plugin/theme integrity | `references/wp-composer.md` |
| General WordPress theme PHP conventions (non-Kaliroots) | `references/wp-theme-dev.md` |
| Kaliroots parent/child theme conventions and overrides | `references/wp-kaliroots.md` |
| ACF field groups, local JSON, options pages, naming, output | `references/wp-acf.md` |
| Theme asset source, Vite/Tailwind build, enqueue integration | `references/wp-assets.md` |
| Lovable → WordPress conversion (Lovable React export into Websimple theme) | `references/wp-lovable.md` |
| Browser-based smoke checks, navigation, SSRF allowlisting | `references/wp-browser.md` |
| Autonomous Xdebug runtime debugging | `references/wp-xdebug.md` |
| PHPCS/PHPCBF tooling and coding-standard checks | `references/wp-code-style.md` |
Many tasks touch more than one area. For example, "sync example.com's production database into my local site" needs `wp-project.md` (to resolve names and paths), `websimple-gitea.md` (to read `WP_SITE_URL` and SSH metadata), and `wp-local-site.md` (the synchronize-from-remote workflow). Read all three before starting.
## Gitea API access — use the bundled script
Three read-only Gitea operations are available via `scripts/gitea.sh`. They replace what used to be OpenClaw plugin tools and require `WEBSIMPLE_GITEA_API_TOKEN` plus `jq` and `curl`.
```bash
# List repositories in the wp-sites org (default org is wp-sites)
bash "${SKILL_ROOT}/scripts/gitea.sh" repos-list
# List Actions variables for a repository
bash "${SKILL_ROOT}/scripts/gitea.sh" vars-list wp-sites example
# Get one Actions variable
bash "${SKILL_ROOT}/scripts/gitea.sh" var-get wp-sites example WP_SITE_URL
```
Each prints a JSON envelope on stdout and exits non-zero with a stderr message on failure. See `references/websimple-gitea.md` for the full operation set and output shapes.
`${SKILL_ROOT}` here is the directory containing this `SKILL.md`. Resolve it from the path of this file when invoking the script.
## Safety defaults
These apply across every reference file. They override anything in a reference that conflicts.
- **Prefer read-only inspection before changing anything.** Before deleting, dropping a database, overwriting files, or running broad updates, show the resolved values (path, DB name, URL) and ask for confirmation.
- **Treat remote hosts as read-only.** Production/staging WordPress sites should only be read from (database export, file rsync) — never written to, never updated, never have plugins deactivated, never have WP-CLI mutations run against them. The only approved remote action shapes are read-only WP-CLI commands (e.g. `wp option get`, `wp db export -`), `rsync` *from* remote *to* local, and SSH connection tests.
- **Never echo secrets.** Do not print `WEBSIMPLE_GITEA_API_TOKEN`, admin passwords, or anything from `~/.my.cnf` or `~/.wp-cli/config.yml` back in chat. When using them, reference them by variable name.
- **Preserve source-controlled files.** When repairing Composer-managed plugins/themes, never delete directories that are tracked in Git — only ignored/disposable install artifacts. See `references/wp-composer.md` for the integrity-check pattern.
- **Use recoverable deletion where possible.** Prefer moving files to the user's trash over `rm -rf` when shelling out, especially for project directories.
## Workflow shape
For a typical request:
1. Identify which reference(s) the task falls under from the table above.
2. Read those reference file(s) — fully, not just headings.
3. Resolve the WordPress project's slug, paths, domain, and DB name using `references/wp-project.md` conventions and the relevant environment variables.
4. If remote metadata is needed (production URL, SSH host/port/user/path), prefer `scripts/gitea.sh var-get` against the matching `wp-sites/<slug>` repository before falling back to SSH inspection.
5. Run small read-only checks before any change.
6. Make the smallest change that accomplishes the request, asking for confirmation before destructive steps.
7. Verify with a small post-change check.
When in doubt about which reference applies, the simple rule: the file's name describes its concern. `wp-acf.md` is for ACF questions, `wp-xdebug.md` is for Xdebug-driven debugging, and so on.

View File

@@ -0,0 +1,70 @@
# Websimple Gitea
Use this skill for Websimple Gitea repository conventions and read-only metadata lookup workflows.
## Base URL
Websimple Gitea is hosted at:
- `https://gitea.websimple.com`
## Authentication
Authenticated Gitea API reads use the `WEBSIMPLE_GITEA_API_TOKEN` environment variable. The bundled `scripts/gitea.sh` wrapper reads this value automatically. Assume the base URL is constant at `https://gitea.websimple.com`.
If `WEBSIMPLE_GITEA_API_TOKEN` is not set in the user's environment, ask them to export it before proceeding. Never print or echo the token's value back in chat. Use it only for read-only Gitea API requests.
## Available operations
The skill bundles a `scripts/gitea.sh` wrapper that exposes three read-only operations. The path is relative to the skill root, so resolve it from the skill directory (typically something like `<skill-root>/scripts/gitea.sh`).
Each operation prints a JSON envelope to stdout and exits non-zero on failure (with an error message on stderr).
- `gitea.sh repos-list [--org <org>]` — list repositories for an organization. Default org is `wp-sites`. Maps to `GET /orgs/{org}/repos`.
- `gitea.sh vars-list <owner> <repo>` — list Actions variables for a repository. Maps to `GET /repos/{owner}/{repo}/actions/variables`.
- `gitea.sh var-get <owner> <repo> <variable-name>` — get one Actions variable. Maps to `GET /repos/{owner}/{repo}/actions/variables/{variablename}`.
Example usage:
```bash
bash "${SKILL_ROOT}/scripts/gitea.sh" var-get wp-sites example WP_SITE_URL
```
Pipe through `jq` if a specific field is needed:
```bash
bash "${SKILL_ROOT}/scripts/gitea.sh" var-get wp-sites example WP_SITE_URL | jq -r '.variable.data'
```
If the wrapper exits non-zero, surface its stderr message to the user rather than retrying blindly; common causes are a missing/expired token or a typo in owner/repo/variable name.
## WordPress repositories
Websimple WordPress site repositories live under the `wp-sites` organization.
For WordPress project slug `${slug}`:
- Repository URL: `https://gitea.websimple.com/wp-sites/${slug}`
## Actions variables
Use Gitea Actions variables as the source of truth for project deployment and remote environment metadata.
For WordPress projects, expect these deployment values, usually from repository Actions variables:
- `WP_SITE_URL`: production WordPress site URL, including protocol, matching the WordPress `siteurl` option concept.
- `REMOTE_HOST`: SSH host for the remote environment.
- `REMOTE_PORT`: SSH port for the remote environment.
- `REMOTE_USER`: SSH user for the remote environment.
- `REMOTE_PATH`: WordPress project path on the remote host.
`wp-sites` repositories may rely on organization/repository fallback variables or secrets for `REMOTE_*` deployment values. If a per-repository `REMOTE_HOST`, `REMOTE_PORT`, `REMOTE_USER`, or `REMOTE_PATH` variable is missing, check the relevant fallback variable/secret source before treating the repository metadata as incomplete.
Prefer reading Gitea deployment metadata before using SSH. SSH lookups are fallback-only when required metadata is missing after repository values and fallback variables/secrets are considered.
## Safety
Treat Gitea metadata reads as read-only. Do not modify repositories, variables, secrets, workflows, branches, tags, issues, releases, or pull requests unless a future workflow explicitly adds an approval path.
Do not expose tokens or secrets. If authentication is needed beyond `WEBSIMPLE_GITEA_API_TOKEN`, ask the user to set the appropriate environment variable rather than printing or storing secrets inline.

View File

@@ -0,0 +1,50 @@
# websimple-stack
Use this skill for host environment, Docker Compose, Traefik routing, and shared tooling concerns surrounding the Websimple local development environment.
The Websimple local development Docker stack is maintained in Gitea:
- https://gitea.websimple.com/docker/websimple-stack
## Config values used here
These environment variables drive stack-level operations:
- `WEBSIMPLE_STACK_PATH`: local path to the `websimple-stack` Docker project checkout.
- `WEBSIMPLE_STACK_PROTOCOL`: protocol used by local Traefik routing, `http` or `https`.
- `WEBSIMPLE_STACK_DOMAIN`: local domain suffix used by Traefik labels.
Do not hard-code these values in commands or examples. Read them from the environment. If any are missing when needed, ask the user to export them before proceeding rather than guessing.
## Environment expectations
The plugin assumes it is running on a developer machine configured for the Websimple local development stack.
### Stack and routing
- The `websimple-stack` Docker Compose project is configured and running locally.
- A wildcard `*.${WEBSIMPLE_STACK_DOMAIN}` resolves to the local IP.
- Traefik serves local stack services at `${WEBSIMPLE_STACK_PROTOCOL}://*.${WEBSIMPLE_STACK_DOMAIN}` using that wildcard domain routing.
### Required host executables
The following executables are expected to be available in the host `$PATH`:
- `mysql`
- `node`
- `pnpm`
- `php`
- `composer`
- `wp`
### MySQL access
- MySQL credentials are stored in `~/.my.cnf`.
- The `mysql` client should connect without prompting for a password.
- `/etc/hosts` contains an entry resolving `mysql`, so the DB host resolves the same way on the host as it does inside containers.
### WP-CLI defaults
- WP-CLI defaults are configured in `~/.wp-cli/config.yml`.
- DB host, user, and password defaults are already set there.
- `wp core config` workflows should only need to specify the database name.

271
references/wp-acf.md Normal file
View File

@@ -0,0 +1,271 @@
# WP ACF
Use this skill for Websimple WordPress projects that use Advanced Custom Fields.
Use `references/wp-theme-dev.md` for general PHP/theme structure, `references/wp-code-style.md` for PHPCS/PHPCBF, `references/wp-browser.md` for visual checks, and `references/wp-xdebug.md` for runtime debugging.
## Core principles
- Keep ACF configuration predictable, portable, and version-controlled.
- Prefer clear field names that map naturally to template usage.
- Keep field registration/ACF hooks under `includes/vendors/acf.php` or the existing ACF vendor/module file.
- Keep rendering logic in templates and reusable data helpers under `includes/` when needed.
- Avoid changing field keys, field names, return formats, or option names casually; these may affect stored content.
## Local JSON
ACF field group JSON definitions should live under the active theme's `acf-json/` directory.
- Keep ACF JSON files version-controlled.
- Preserve the project's existing JSON directory if one exists.
- For greenfield projects, use `acf-json/` at the theme root.
- Ensure the `acf-json/` directory exists and is writable by WordPress.
- Do not delete or regenerate ACF JSON files without checking the diff.
When local JSON is enabled, saving a field group, post type, taxonomy, or options page in wp-admin automatically creates or updates its JSON file. Do not tell the user to manually re-export JSON after normal dashboard saves; instead, remind them to review and commit the changed JSON file.
There is no expected per-field-group `json_sync` flag in Websimple conventions. ACF local JSON is controlled by ACF's global JSON setting and save/load paths. ACF defaults local JSON on and uses the active theme's `acf-json/` folder unless customized.
If PHP hooks customize ACF JSON paths, keep them in the ACF vendor/module file, e.g. `includes/vendors/acf.php`.
## Field group keys and filenames
For greenfield field groups, use deterministic field group keys instead of ACF's auto-generated group keys.
The field group key should match the JSON filename and describe what the group targets:
```text
acf-json/group_post_project.json
key: group_post_project
```
Examples:
```text
group_post_project # fields for the project custom post type
group_page_home # fields for the home page/template
group_options_site # site options fields
group_taxonomy_sector # fields for the sector taxonomy
```
ACF field keys inside the group may remain auto-generated. The deterministic-key convention applies primarily to field groups so JSON files stay organized and easy to find.
For existing projects with auto-generated field group keys, preserve them unless the user explicitly approves a cleanup/migration. Do not rename existing field group keys casually because synced JSON, database references, and editor workflows may depend on them.
## Synchronizing ACF JSON
For ACF 6.8+ with WP-CLI available, prefer the built-in ACF JSON CLI commands.
Safe inspection:
```bash
wp acf json status
wp acf json sync --dry-run
```
Apply pending local JSON changes to the database:
```bash
wp acf json sync
```
Useful scoped variants:
```bash
wp acf json sync --type=field-group
wp acf json sync --key=group_post_project
```
Notes:
- `wp acf json sync` modifies the database; run `--dry-run` first, especially outside local development.
- The command syncs pending local JSON changes for ACF item types such as field groups, post types, taxonomies, and options pages.
- If no items are pending, expect an “already in sync” style result.
- Do not use a guessed `--all` flag; current ACF CLI sync defaults to all supported item types unless scoped with `--type` or `--key`.
If the project uses an older ACF/ACF PRO version without these commands:
1. Check the installed ACF version and whether an update is safe for the project.
2. If updating is low-risk and explicitly approved, update ACF/ACF PRO first, then use the CLI workflow.
3. Otherwise, use `references/wp-browser.md` to navigate wp-admin and sync from the ACF field group/admin screen.
For production or staging, ask before updating plugins or syncing database-backed ACF changes.
## Field group organization
Use field groups that reflect editorial concepts and template needs.
Common grouping patterns:
- page/template-specific fields;
- reusable section/component fields;
- site options;
- post type-specific metadata;
- taxonomy/user/menu-item metadata when needed.
Avoid dumping unrelated fields into a single large group. Prefer labels and instructions that make sense to editors.
## Field naming
Use stable, descriptive, snake_case field names.
Good examples:
```text
hero_title
hero_text
hero_image
cta_link
featured_posts
background_color
```
Avoid vague names like `title_2`, `content_block`, or `misc` unless the existing project convention requires it.
Do not rename existing field names unless the user accepts the content migration risk.
## Return formats
Respect existing return formats. Changing them can break templates.
Common preferences:
- image fields: use the format already expected by the theme; arrays are useful when templates need alt/title/sizes;
- link fields: arrays are usually practical for URL/title/target;
- post object / relationship fields: know whether the field returns IDs or objects before using it;
- true/false fields: handle empty/false explicitly.
When uncertain, inspect the field group JSON or runtime value before changing template code.
## Template usage
Prefer `get_field()` when the value needs escaping, conditionals, fallback handling, or reuse.
```php
<?php $title = get_field( 'hero_title' ); ?>
<?php if ( $title ) : ?>
<h1><?= esc_html( $title ); ?></h1>
<?php endif; ?>
```
Use compact output tags with escaping, following `references/wp-theme-dev.md`:
```php
<?= esc_html( get_field( 'eyebrow' ) ); ?>
<?= wp_kses_post( get_field( 'intro' ) ); ?>
```
Avoid `the_field()` for dynamic frontend output unless the value is known safe for the exact context. Prefer explicit escaping with `get_field()`.
For image arrays:
```php
<?php $image = get_field( 'hero_image' ); ?>
<?php if ( $image ) : ?>
<img
src="<?= esc_url( $image['url'] ); ?>"
alt="<?= esc_attr( $image['alt'] ?? '' ); ?>"
>
<?php endif; ?>
```
For link arrays:
```php
<?php $link = get_field( 'cta_link' ); ?>
<?php if ( $link ) : ?>
<a href="<?= esc_url( $link['url'] ); ?>" target="<?= esc_attr( $link['target'] ?: '_self' ); ?>">
<?= esc_html( $link['title'] ); ?>
</a>
<?php endif; ?>
```
## Options pages
Keep ACF options-page registration in the ACF vendor/module file.
Use options pages for true site-wide settings, not content that belongs to a page, post, taxonomy, or theme template.
When reading option fields, make the option scope explicit:
```php
<?php $phone = get_field( 'phone_number', 'option' ); ?>
```
## Flexible content and repeaters
Keep flexible content layouts readable and component-oriented.
- Use layout names that map to template partials or sections.
- Keep layout fields scoped to what the layout renders.
- Avoid deeply nested repeaters/flexible fields unless there is a strong editorial reason.
- For complex flexible content rendering, prefer dispatching to template parts instead of one giant template file.
The dispatch pattern depends on the theme. **Check whether the theme is a Kaliroots child theme first** (see `references/wp-kaliroots.md`) — Kaliroots has its own template lookup helpers and overriding them with generic WP dispatch fragments the resolution path.
For a **generic (non-Kaliroots) WordPress theme**:
```php
<?php if ( have_rows( 'sections' ) ) : ?>
<?php while ( have_rows( 'sections' ) ) : the_row(); ?>
<?php get_template_part( 'templates/content/sections/' . get_row_layout() ); ?>
<?php endwhile; ?>
<?php endif; ?>
```
For a **Kaliroots child theme**, dispatch through the Kaliroots section helper so child/parent override resolution works correctly:
```php
<?php if ( have_rows( 'sections' ) ) : ?>
<?php while ( have_rows( 'sections' ) ) : the_row(); ?>
<?= kaliroots_section_template( get_row_layout() ); ?>
<?php endwhile; ?>
<?php endif; ?>
```
In a Kaliroots project, the section partial then lives at `templates/content/sections/{layout-name}.php` and is located via Kaliroots' lookup pipeline — see `references/wp-kaliroots.md` for the helper set and naming rules.
Verify that the target partial exists before introducing either dispatch into an existing theme. If the project already uses a different established pattern, follow that — don't reorganize.
Inside the layout partial, use `get_sub_field()` (not `get_field()`) to read layout sub-fields, and escape at output:
```php
<?php
$heading = get_sub_field( 'heading' );
$image = get_sub_field( 'image' );
?>
<?php if ( $heading ) : ?>
<h2><?= esc_html( $heading ); ?></h2>
<?php endif; ?>
```
## Relationship and post object fields
Before using relationship/post object fields, confirm whether they return IDs or post objects.
- If using objects, restore global post state after setup.
- If using IDs, use explicit IDs with WordPress APIs.
- Avoid expensive relationship queries inside repeated templates when values can be prepared once.
## Refactoring workflow
1. Inspect the active theme, ACF plugin availability, existing ACF JSON path, and ACF-related include files.
2. Inspect existing field group JSON before changing field names, keys, return formats, or locations.
3. Keep PHP hooks/registration in the existing ACF module, usually `includes/vendors/acf.php`.
4. Keep template output escaped explicitly with `get_field()` where practical.
5. Make small, reversible changes.
6. After wp-admin field group edits, inspect the automatically updated `acf-json/` diff.
7. Run PHP lint/style checks via `references/wp-code-style.md` when PHP files changed.
8. Smoke-test affected frontend/admin behavior with `references/wp-browser.md` when relevant.
9. Inspect the diff, especially ACF JSON changes.
## Safety
- Do not rename existing field names or change return formats without calling out content/template impact.
- Do not delete ACF JSON files without explicit approval.
- Do not expose secrets from options pages or database values.
- Do not assume `the_field()` output is safe; escape based on context.
- Preserve the project's existing ACF structure when it is coherent.

155
references/wp-assets.md Normal file
View File

@@ -0,0 +1,155 @@
# WP Assets
Use this skill for Websimple WordPress theme asset source, build, and enqueue conventions.
Use `references/wp-theme-dev.md` for general PHP/theme structure, `references/wp-code-style.md` for PHP linting, and `references/wp-browser.md` for visual smoke tests.
## Core principles
- Keep build-time source assets under `src/`.
- Do not edit compiled/generated assets unless the user explicitly asks and there is no source available.
- Detect the project's actual pipeline before changing files.
- Preserve the existing asset toolchain unless the user asks for migration.
- Keep PHP enqueue integration aligned with the build output and manifest strategy.
## Source structure
Theme source assets should live under `src/` with clear subdirectories for frontend concerns.
Common structure:
```text
src/
styles/ # CSS/SCSS source
scripts/ # JavaScript/TypeScript entrypoints and modules
components/ # Vue/React or shared frontend components
images/ # source images/icons when processed by the build
fonts/ # source fonts when processed by the build
```
Follow the existing project structure first. Do not reorganize assets broadly without approval.
## Detect the pipeline
Before editing assets, inspect the theme root for signals:
```text
package.json
vite.config.*
webpack.config.*
webpack.mix.js
postcss.config.*
tailwind.config.*
tsconfig.json
src/
assets/
dist/
```
Use `package.json` scripts as the source of truth for build/dev commands.
Common signals:
- Vite: `vite.config.ts`, scripts like `dev`, `build`, `preview`.
- Legacy webpack/Mix: `webpack.config.js`, `webpack.mix.js`, scripts using `webpack`, `mix`, or older build tooling.
- Plain scripts/styles: source under `src/` with minimal bundler config.
If signals conflict, report the ambiguity before changing build config.
## Build output
Treat build output as generated unless the project clearly uses checked-in compiled assets.
Common output directories:
```text
assets/
dist/
```
Compiled/generated assets should normally be excluded from Git. For new or modernized projects, prefer ignoring generated build output and committing only source/config files.
Legacy projects may already commit compiled assets. Do not remove those generated files or change `.gitignore` behavior without approval. For those projects, keep the committed-build convention and make sure to run the build before committing so generated assets stay in sync with source changes.
Do not edit minified files, hashed files, generated manifests, compiled CSS/JS, source maps, or copied vendor assets directly when source files exist.
If compiled assets are committed in the project, update them by running the project build instead of manual edits.
## Vite conventions
For Vite-based themes:
- inspect `vite.config.*` before adding entrypoints;
- keep entrypoints under `src/`;
- preserve existing manifest/output settings;
- use the manifest when PHP enqueue code relies on hashed filenames;
- run the project build after changing source assets when verification requires compiled output.
Do not rewrite Vite config or migrate legacy projects to Vite unless explicitly requested.
## Legacy webpack conventions
For legacy webpack/Mix themes:
- inspect `webpack.config.*` or `webpack.mix.js` first;
- preserve existing entrypoint names and output paths;
- avoid modernizing syntax or build tooling as part of unrelated changes;
- run the existing build script when compiled assets must be refreshed.
Do not convert webpack/Mix to Vite without an explicit migration request.
## Enqueue integration
Asset enqueue logic usually belongs under theme PHP includes, often `includes/core/assets.php` or the existing project equivalent.
Before changing enqueue code:
1. Identify the build output path.
2. Identify whether filenames are stable or hashed.
3. Identify whether the theme uses a manifest.
4. Preserve existing handles, dependencies, script type/module settings, and localization/global variables unless changing them is required.
For stable filenames, filemtime-based cache busting is acceptable when already used by the project:
```php
wp_enqueue_style(
'example-theme',
get_stylesheet_directory_uri() . '/dist/css/theme.css',
[],
filemtime( get_stylesheet_directory() . '/dist/css/theme.css' )
);
```
For hashed filenames, read from the manifest rather than hardcoding generated names.
## Vue and React source
Vue/React source should still live under `src/`, typically under `src/components/` or an existing frontend app directory.
Keep this skill focused on source placement and build integration. For component architecture, state management, app mounting, or framework-specific conventions, use dedicated future `wp-vue` or `wp-react` guidance.
## Styling conventions
- Keep styles under `src/styles/` or the existing style source directory.
- Preserve the project's CSS methodology and naming conventions.
- Avoid broad restyling when the user asked for a targeted fix.
- Prefer source-level changes over compiled CSS edits.
## Workflow
1. Inspect theme root, `package.json`, build config, source directories, output directories, and enqueue PHP.
2. Determine whether the project uses Vite, webpack/Mix, or another pipeline.
3. Edit source files under `src/` or the established source directory.
4. Do not touch generated output manually.
5. Run the smallest relevant check: build, typecheck, lint, or targeted frontend smoke test.
6. If compiled assets are committed, run the build before committing and inspect generated diffs.
7. Use `references/wp-browser.md` to verify visible frontend changes.
8. Report changed source files, generated files, and any build/verification blockers.
## Safety
- Do not run package installs or major upgrades without approval.
- Do not migrate build tools without explicit request.
- Do not edit compiled/minified/generated assets directly when source exists.
- Do not remove legacy build config just because a newer tool would be nicer.
- Watch for large generated diffs and call them out before committing.

77
references/wp-browser.md Normal file
View File

@@ -0,0 +1,77 @@
# WP Browser
Use this skill for WordPress tasks that require a real browser instead of WP-CLI or file inspection. Prefer `references/wp-project.md` first when you need to resolve a project slug, local URL, production URL, repo, or paths.
## Safety and scope
- Treat local sites as safe to inspect and operate, but still avoid destructive admin actions unless explicitly requested.
- Ask before changing production content, settings, users, plugins, themes, orders, forms, menus, options, or publishing state.
- Prefer read-only checks on remote/production sites: navigate, inspect, screenshot, verify text/layout, check console/network symptoms.
- Do not submit public forms, checkout flows, newsletter signups, contact forms, or anything that sends email/external effects unless explicitly requested.
- If login is required, use existing browser session/cookies when available. Ask for credentials only if no usable session exists.
## Choosing the URL
Resolve the target URL from the request:
- If the user gives a full URL, use it directly.
- If the user gives a project slug, use `references/wp-project.md` conventions to derive the local site URL: `${WEBSIMPLE_STACK_PROTOCOL}://${slug}.${WEBSIMPLE_STACK_DOMAIN}`.
- If the user says production/remote, prefer `WP_SITE_URL` from Gitea repository Actions variables via `references/websimple-gitea.md` conventions/tools when available.
- If the request is ambiguous between local and production, default to local and state that assumption briefly.
## Browser environment prerequisite
Websimple local dev sites may resolve to private network IPs, for example a `*.${WEBSIMPLE_STACK_DOMAIN}` hostname resolving to an IPv6 ULA address like `fd00::/8`. Some browser/MCP harnesses are SSRF-guarded and block private-network targets by default.
If browser navigation to a local Websimple site fails with a policy-related error (e.g. "navigation blocked by policy"), the browser harness likely needs an allowlist entry for the local Websimple domain plus private-network opt-in. The shape of that configuration depends on the harness in use (Playwright MCP, OpenClaw, etc.), but conceptually:
- Allow `*.${WEBSIMPLE_STACK_DOMAIN}` (and `${WEBSIMPLE_STACK_DOMAIN}` itself) on the hostname allowlist.
- Permit private-network destinations.
Resolve `${WEBSIMPLE_STACK_DOMAIN}` from the environment; do not hard-code a specific local domain. Prefer an allowlisted form over globally allowing private-network access. Restart/reload the browser harness after changing its SSRF policy.
## Browser workflow
1. Open or reuse a browser tab for the target site.
2. Take a snapshot before interacting; use semantic/ARIA refs where possible.
3. Navigate like a user: click links/buttons, fill inputs, use menus, and wait for reliable UI state instead of arbitrary delays.
4. Check the page result with at least one concrete signal: visible text, URL, status page, screenshot, console messages, or DOM state.
5. Report concise evidence: URL checked, action performed, result, and any blockers.
For multi-step browser flows, login checks, stale refs, or tab recovery, follow the general `browser-automation` skills browser-control practices.
## WordPress admin
Common admin URLs:
- Dashboard: `/wp-admin/`
- Login: `/wp-login.php`
- Plugins: `/wp-admin/plugins.php`
- Themes: `/wp-admin/themes.php`
- Customizer: `/wp-admin/customize.php`
- Menus: `/wp-admin/nav-menus.php`
- Pages: `/wp-admin/edit.php?post_type=page`
- Posts: `/wp-admin/edit.php`
- Site Health: `/wp-admin/site-health.php`
When checking admin screens:
- Confirm whether the session is authenticated by looking for the admin bar, dashboard, or login form.
- Avoid saving forms unless requested.
- For settings screens, inspect current values and controls before editing.
- For editor screens, avoid autosave-risky content edits unless explicitly requested.
- If normal browser `fill` does not populate the WordPress login fields, use a narrow DOM fallback to set `#user_login` and `#user_pass`, dispatch `input` events, then submit. Do not echo credentials back to the user.
## Frontend checks
For frontend verification:
- Check desktop first unless the user asks for mobile/responsive.
- Capture a screenshot when visual appearance matters.
- Inspect console errors when behavior looks broken or JavaScript-heavy.
- Verify canonical symptoms with user-visible evidence, not assumptions from source code alone.
## Cookies and debugging hooks
- For Xdebug-specific browser requests, use `references/wp-xdebug.md`.
- If needed manually, set or include the `XDEBUG_SESSION=vscode` cookie only for local debugging flows and mention that it may route PHP through `wp-php-xdebug`.

View File

@@ -0,0 +1,62 @@
# WP Code Style
Use this skill for PHP source formatting and coding-standard checks in Websimple WordPress projects.
## Shared tooling model
Websimple WordPress projects should rely on shared/global Websimple PHPCS tooling, not per-project Composer installs.
Project repos should keep only project-specific lint scope in `phpcs.xml`. The PHPCS binary, PHPCBF binary, and `WebsimpleWP` ruleset should come from global Composer tooling.
Expected global setup, only when explicitly asked to install or repair tooling:
```bash
composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer global require squizlabs/php_codesniffer lewebsimple/wp-phpcs-ruleset
```
If global Composer binaries are not on `PATH`, locate them with:
```bash
composer global config bin-dir --absolute --quiet
```
When cleaning an existing project, remove per-project Composer ownership of PHP_CodeSniffer and Websimple coding-standard tooling:
- remove `squizlabs/php_codesniffer` from `require-dev` when present;
- remove `lewebsimple/wp-phpcs-ruleset` from `require-dev` when present;
- remove `lint` / `lintfix` Composer scripts when they only wrap project-local PHPCS/PHPCBF, such as `vendor/bin/phpcs` or `vendor/bin/phpcbf`.
Preserve unrelated Composer dependencies and scripts. If a script does more than run PHPCS/PHPCBF, ask before removing or rewriting it.
## Required phpcs.xml
Make sure `phpcs.xml` exists at the project root and only includes `wp-content/mu-plugins/` plus the active/current theme directory:
```xml
<?xml version="1.0"?>
<ruleset name="wp-code-style">
<rule ref="WebsimpleWP"/>
<file>wp-content/mu-plugins/</file>
<file>wp-content/themes/${theme}/</file>
</ruleset>
```
Resolve `${theme}` from the active child/current theme or from the users target. Do not lint all themes, WordPress core, vendor, uploads, cache, generated assets, or unrelated directories.
## Workflow
1. Inspect `composer.json`, `composer.lock`, and any existing `phpcs.xml` / `phpcs.xml.dist`.
2. Resolve the target theme with WP-CLI or source inspection.
3. Remove project-local PHPCS/ruleset Composer dependencies and simple `lint` / `lintfix` scripts when present.
4. Ensure `phpcs.xml` is present and scoped correctly.
5. Run lint with global `phpcs` from the shell, not `vendor/bin/phpcs`.
6. Use global `phpcbf` only when the user requested formatting or the fix is clearly safe.
7. Re-run lint after fixes and inspect the diff before claiming success.
## Safety
- Do not run PHPCBF across broad paths unless the `phpcs.xml` scope is correct.
- Do not modify vendor, WordPress core, uploads, caches, or compiled assets.
- If PHPCBF changes files, summarize changed files and remaining PHPCS issues.
- Do not remove Composer scripts that contain extra behavior beyond straightforward PHPCS/PHPCBF wrappers without asking first.

134
references/wp-composer.md Normal file
View File

@@ -0,0 +1,134 @@
# WP Composer
Use this skill for Composer-based dependency management in Websimple WordPress projects.
## Context
Websimple manages WordPress plugins and themes with PHP Composer.
Private Websimple Composer packages are served by Satis at:
- `https://satis.ledevsimple.ca`
For WordPress project conventions such as slug, local path, local URL, database name, and repository URL, use `references/wp-project.md`. For host tooling assumptions, including the `composer` executable being available in `$PATH`, use `references/websimple-stack.md`.
## Operating guidance
- Work from the local project root containing `composer.json`.
- Prefer read-only inspection commands before making dependency changes.
- Inspect `composer.json` and `composer.lock` directly when answering dependency questions.
- Use Composer commands through the host `composer` executable unless a task explicitly requires container context.
- Treat dependency changes as project code changes: inspect the resulting `composer.json` and `composer.lock`, then run a small verification command.
- Ensure `wp-config.php` loads Composer vendor libraries when a project uses Composer.
- Composer-managed WordPress plugins/themes should be ignored by Git. Some projects also have project-specific plugins/themes committed with the WordPress project; do not delete or overwrite source-controlled plugins/themes when repairing Composer-managed dependencies.
## Common read-only checks
Use these before changing dependencies:
```bash
composer validate --no-check-all
composer show
composer show vendor/package
composer outdated --direct
composer why vendor/package
composer why-not vendor/package:^1.2
```
Use `composer validate --no-check-all` by default because existing Websimple WordPress projects may intentionally use `"*"` version constraints for WordPress plugins/themes, and plain `composer validate` complains about unbound constraints.
## Package repositories
Composer projects should have Composer repository entries for both Satis (private repository) and WPackagist (official WordPress package repository).
The `repositories` section of `composer.json` should include:
```json
{
"repositories": [
{
"type": "composer",
"url": "https://satis.ledevsimple.ca"
},
{
"type": "composer",
"url": "https://wpackagist.org"
}
]
}
```
Do not assume every project already has these repositories configured. Check `composer.json` first.
Repository priority order matters: check Websimple Satis first, then WPackagist. Private Websimple packages should win over public package sources when names overlap.
## Dependency changes
When asked to add, update, or remove a WordPress plugin/theme package:
1. Confirm the target project path using `references/wp-project.md` conventions.
2. Inspect `composer.json` and `composer.lock`.
3. Verify package resolution priority before adding or updating packages: Websimple Satis first, then WPackagist.
4. Run the smallest Composer command that performs the requested change.
5. Inspect the diff for `composer.json` and `composer.lock`.
6. Run `composer validate --no-check-all` after the change.
Examples:
```bash
composer require vendor/package:^1.2
composer update vendor/package --with-dependencies
composer remove vendor/package
composer validate --no-check-all
```
Ask before running broad updates such as `composer update` without package names.
## WordPress Composer autoload
When a WordPress project uses Composer, ensure `wp-config.php` contains this Composer autoloader guard so WordPress can load Composer-managed vendor libraries:
```php
if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {
require_once __DIR__ . '/vendor/autoload.php';
}
```
Add it only if missing, and avoid duplicating equivalent autoload logic.
Place the guard near the top of `wp-config.php`, after the opening `<?php` and any file header/comments, before WordPress settings/constants and before this line:
```php
require_once ABSPATH . 'wp-settings.php';
```
This keeps Composer classes available to project configuration and WordPress bootstrap code.
## Plugins/themes integrity check
Composer-based WordPress projects can drift when a plugin/theme installed under `wp-content/plugins` or `wp-content/themes` no longer matches the version/source recorded in `composer.lock` (for example after a WordPress dashboard update, manual file edits, interrupted install, or broken vendor state).
When diagnosing plugin/theme drift:
1. Inspect `composer.lock` to identify packages installed into `wp-content/plugins/*` and `wp-content/themes/*`.
2. Check Git status before touching files so source-controlled project-specific plugins/themes are preserved.
3. Treat Git-ignored Composer-managed plugin/theme directories as disposable install artifacts.
4. Prefer read-only checks first, such as `composer install --dry-run` when useful, `composer show --locked`, and direct inspection of installed package metadata when available.
5. If integrity is uncertain, prefer a clean reinstall of Composer-managed plugins/themes from repositories rather than trying to patch files in place.
Safe repair pattern:
```bash
composer install --no-interaction --prefer-dist
```
If a fresh install is required, remove only Composer-managed, Git-ignored plugin/theme directories and reinstall from `composer.lock`. Do not remove project-specific plugin/theme directories that are tracked by Git.
Before deleting any plugin/theme directory, verify both:
```bash
git check-ignore -q wp-content/plugins/example-plugin
git ls-files --error-unmatch wp-content/plugins/example-plugin >/dev/null 2>&1
```
Only a path that is ignored and not tracked should be considered disposable. Ask before deleting directories, even when they appear disposable.

249
references/wp-kaliroots.md Normal file
View File

@@ -0,0 +1,249 @@
# WP Kaliroots
Use this skill when a Websimple WordPress site uses the `kaliroots` parent theme or when behavior may come from Kaliroots parent-theme code.
Kaliroots source of truth: `https://gitea.websimple.com/wp-themes/kaliroots`.
Use `references/wp-theme-dev.md` for general theme PHP conventions that do not conflict with Kaliroots. Use `references/wp-browser.md` for frontend checks and `references/wp-xdebug.md` when parent/child template resolution or hooks are unclear.
## Core rule
For Kaliroots child themes, do not apply generic WordPress template organization blindly. Kaliroots has its own wrapper and template lookup system.
Prefer child-theme overrides and additions. Treat parent theme changes as higher-risk because they can affect many sites.
## Confirm Kaliroots usage
Before applying Kaliroots conventions, confirm the active theme relationship:
```bash
wp theme list --status=active
wp theme get $(wp option get stylesheet) --field=template
```
Or inspect the child theme `style.css` for a parent declaration like:
```css
Template: kaliroots
```
Inspect the child theme first, then inspect the parent theme only when inherited behavior or helper functions matter.
## Parent theme structure
Kaliroots parent uses a compact structure:
```text
functions.php
includes/
core/
purgecss.php
theme-setup.php
theme-wrapper.php
helpers/
assets.php
attachment.php
datetime.php
html.php
input.php
mail.php
media.php
menu.php
meta.php
mutex.php
query.php
shortcodes.php
social.php
taxonomy.php
template.php
user.php
utilities.php
wpfs.php
vendors/
acf.php
gutenberg.php
wpbakery.php
templates/
html/
head.php
mail.php
wrap.php
content/
main.php
```
The parent `functions.php` is a loader for `includes/core`, `includes/helpers`, and `includes/vendors`.
Child themes may add their own `includes/`, `templates/`, and asset sources. Preserve existing child-theme conventions unless the user asks for cleanup.
## Template wrapper model
Kaliroots wraps base WordPress templates through the `template_include` filter.
The parent wrapper flow is:
1. `template_include` calls `kaliroots_wrap_base_template()`.
2. The wrapper locates `templates/html/wrap-{base}.php` through `kaliroots_locate_template()`.
3. `{base}` is expanded by `kaliroots_base_placeholders()` using WordPress conditional context.
4. `locate_template()` gives child themes priority over parent templates.
Parent default wrapper:
```php
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<?= kaliroots_html_template( 'head' ); ?>
<body class="<?= join( ' ', get_body_class() ) ?>">
<div id="app">
<main>
<?= kaliroots_generic_template( '{base}' ) ?>
</main>
</div>
<?php wp_footer(); ?>
</body>
</html>
```
When changing page layout in a Kaliroots child theme, prefer overriding or adding wrapper/content templates in the child theme instead of replacing standard WordPress top-level templates without checking how Kaliroots resolves them.
## Template helper functions
Kaliroots provides template helper functions in `includes/helpers/template.php`.
Common helpers:
```php
kaliroots_generic_template( $template_path, $vars = [], $args = [] );
kaliroots_html_template( $template_name, $vars = [], $args = [] );
kaliroots_site_template( $template_name, $vars = [], $args = [] );
kaliroots_content_template( $template_name, $vars = [], $args = [] );
kaliroots_loop_template( $template_name, $vars = [], $args = [] );
kaliroots_section_template( $template_name, $vars = [], $args = [] );
kaliroots_shortcode_template( $template_name, $vars = [], $args = [] );
kaliroots_widget_template( $template_name, $vars = [], $args = [] );
```
These helpers locate templates through the Kaliroots lookup pipeline, inject `$vars` using `set_query_var()`, buffer template output, and can optionally cache rendered output.
Example:
```php
<?= kaliroots_content_template( 'hero', [ 'post_id' => get_the_ID() ] ); ?>
```
Inside the located template, read injected values with `get_query_var()` when needed:
```php
<?php $post_id = get_query_var( 'post_id' ); ?>
```
## Template naming and lookup
Kaliroots lookup supports placeholders:
- `{base}`: expanded using the current WordPress conditional/template context.
- `{post_type}`: expanded using `get_post_type()` when available.
Examples:
```php
kaliroots_html_template( 'head' );
// looks for templates/html/head-{base}.php, then falls back through base placeholder variants.
kaliroots_content_template( 'main' );
// looks for templates/content/main-{base}.php variants.
kaliroots_loop_template( 'default' );
// looks for templates/content/loops/default-{post_type}.php, then fallback variants.
```
When adding child-theme templates, prefer Kaliroots-compatible paths and names:
```text
templates/html/wrap-front-page.php
templates/html/wrap.php
templates/content/main-front-page.php
templates/content/main-single-project.php
templates/content/loops/default-project.php
```
Inspect the actual child theme before adding a new path; some projects may already have established naming.
## Extending template resolution
Kaliroots exposes filters for template lookup.
Use these only when adding a reusable lookup convention is clearer than adding explicit templates:
```php
add_filter( 'kaliroots_locate_templates', 'example_kaliroots_locate_templates' );
function example_kaliroots_locate_templates( array $templates ): array {
$templates[] = 'templates/content/custom-fallback';
return $templates;
}
```
```php
add_filter( 'kaliroots_base_placeholders', 'example_kaliroots_base_placeholders' );
function example_kaliroots_base_placeholders( array $templates ): array {
if ( is_post_type_archive( 'project' ) ) {
array_unshift( $templates, 'archive-project-featured' );
}
return $templates;
}
```
Keep these filters rare and well-named; they affect template resolution globally.
## Helpers and inherited behavior
Before reimplementing helper logic in a child theme, inspect Kaliroots helpers under `includes/helpers/`.
Useful parent helpers include:
- asset registration with filemtime cache busting;
- template loading and pagination;
- menu/media/meta/query helpers;
- HTML cleaning helpers;
- shortcode and utility helpers.
Do not assume helper behavior from memory. Inspect the parent helper before using or overriding it.
## Parent vs child changes
Default to child-theme changes:
- child template override;
- child `includes/` function/hook;
- child asset/source change;
- child-specific filter/action.
Only edit Kaliroots parent when the user explicitly asks to change shared parent behavior or the bug truly belongs in the parent.
Before parent edits, report blast radius:
- which projects/sites may inherit the change;
- whether a child override is safer;
- how behavior will be verified.
## Refactoring workflow
1. Confirm active child theme and Kaliroots parent.
2. Inspect child theme overrides first.
3. Inspect relevant Kaliroots parent files: usually `includes/core/theme-wrapper.php` and `includes/helpers/template.php`.
4. Choose child override vs parent change deliberately.
5. Keep changes small and reversible.
6. Run PHP lint/style checks via `references/wp-code-style.md` when PHP files changed.
7. Use `references/wp-browser.md` for frontend smoke checks.
8. Use `references/wp-xdebug.md` if template resolution/hook flow is unclear.
9. Inspect the diff before claiming success.
## Safety
- Do not apply generic `references/wp-theme-dev.md` template structure when Kaliroots lookup rules apply.
- Do not edit parent Kaliroots casually; prefer child overrides.
- Do not mass-reorganize child templates without approval.
- Do not touch core, vendor, uploads, caches, or compiled assets.
- Preserve existing project conventions when they are coherent.

214
references/wp-local-site.md Normal file
View File

@@ -0,0 +1,214 @@
# WP Local Site
Use this skill for local WordPress site lifecycle operations. For project naming and derived values, use `references/wp-project.md`. For stack/host environment assumptions, use `references/websimple-stack.md`. For Composer-managed plugins/themes, use `references/wp-composer.md`.
## Derived values
For project slug `${slug}`, use `references/wp-project.md` conventions:
- Local project path: `${WP_LOCAL_ROOT_PATH}/${slug}`
- Local database name: `wp_${slug}`
- Local site URL: `${WEBSIMPLE_STACK_PROTOCOL}://${slug}.${WEBSIMPLE_STACK_DOMAIN}`
- Default local DB table prefix: `wp_`, unless the production/source instance uses a different prefix
- Repository URL: `https://gitea.websimple.com/wp-sites/${slug}`
Resolve project values with `references/wp-project.md`; do not hard-code local paths, protocol, or domain.
## Safety
- Prefer read-only checks before creating, deleting, or resetting anything.
- Ask before destructive actions such as dropping a database, deleting a project directory, or overwriting local files.
- Use recoverable deletion where practical. If shelling out, prefer moving to the user's trash over permanent removal.
- Never delete source-controlled project-specific plugins/themes as part of Composer repair; use the `references/wp-composer.md` integrity guidance.
- Treat remote hosts as read-only unless a future approved workflow says otherwise.
## Create local site skeleton
A local site creation workflow should generally:
1. Resolve `${slug}` and derived values with `references/wp-project.md`.
2. Verify `${WP_LOCAL_ROOT_PATH}` exists.
3. Verify `${WP_LOCAL_ROOT_PATH}/${slug}` does not already exist, or ask how to proceed.
4. Clone or create the project directory from `https://gitea.websimple.com/wp-sites/${slug}`.
5. Create the local database `wp_${slug}` using host `mysql` credentials from `~/.my.cnf`.
6. Determine the DB table prefix: use `wp_` by default, or the production/source prefix when provisioning from an existing site.
7. Run `wp core config` with only the database name and table prefix when WP-CLI defaults provide DB host/user/password.
8. Run Composer install if the project uses Composer.
9. Either run `wp core install` for a fresh local site or synchronize/import from an existing remote site.
10. Verify local URL and WP-CLI access.
Example database creation command:
```bash
mysql -e 'CREATE DATABASE `wp_example` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
```
Use the real derived DB name; quote identifiers safely.
## WP core config
The host should have WP-CLI defaults in `~/.wp-cli/config.yml` for DB host, user, and password. Local provisioning should normally only specify the DB name:
```bash
wp core config --dbname="wp_${slug}" --dbprefix="wp_"
```
Run from the local project path. Do not duplicate DB host/user/password flags unless defaults are missing or the user explicitly asks.
Use `wp_` as the default table prefix for new local sites. When provisioning from production/staging, detect and preserve the source table prefix if it differs from `wp_`; the imported database and local `wp-config.php` prefix must agree.
## WP core install
Use `wp core install` only when creating a fresh local site without importing an existing database. Do not run it before a remote DB import because the import will replace the local tables anyway.
Example for a new local site:
```bash
wp core install \
--url="${WEBSIMPLE_STACK_PROTOCOL}://${slug}.${WEBSIMPLE_STACK_DOMAIN}" \
--title="${slug}" \
--skip-email
```
`~/.wp-cli/config.yml` contains default admin user, email, and password values, so do not pass `--admin_user`, `--admin_email`, or `--admin_password` unless the user asks for custom values. Avoid inventing real production credentials.
## Standard admin user
Local sites should have the standard admin user from `~/.wp-cli/config.yml`. After installing or importing a site, check whether that user exists. If missing, create it using the configured default admin values.
Do not hard-code admin credentials in commands. Do not echo the password in chat or log summaries. Refer to credentials by variable name only.
The exact YAML shape varies by user setup. Inspect the file before constructing any command, so the keys you read match the keys that actually exist:
```bash
# Inspect — show keys only, not the password value
grep -E '^[[:space:]]*(user_login|user_email|user_pass|role)' ~/.wp-cli/config.yml \
| sed 's/\(user_pass:\).*/\1 <redacted>/'
```
Two common shapes:
```yaml
# Shape A — defaults nested under `user create:`
user create:
user_login: someuser
user_email: someone@example.com
user_pass: <set>
role: administrator
```
```yaml
# Shape B — flat top-level keys consumed by custom commands
admin_user: someuser
admin_email: someone@example.com
admin_password: <set>
```
If the file uses Shape A, WP-CLI will already apply the defaults when `wp user create` runs without explicit flags. If it uses Shape B, the values have to be passed explicitly.
When `yq` is available, prefer it over hand-parsing:
```bash
ADMIN_USER="$(yq '.["user create"].user_login // .admin_user' ~/.wp-cli/config.yml)"
ADMIN_EMAIL="$(yq '.["user create"].user_email // .admin_email' ~/.wp-cli/config.yml)"
# Keep ADMIN_PASSWORD only in the subshell that needs it; do not export or print it.
```
If `yq` is not available and the YAML doesn't match a known shape, ask the user for the admin login and email rather than guessing — and have them provide the password via stdin (`read -s`) instead of an argument that would land in shell history.
Typical flow:
1. Inspect `~/.wp-cli/config.yml` and identify the admin user/email key names actually used.
2. Check whether that user exists locally.
3. Create the user only if missing.
4. Grant the administrator role if needed.
```bash
# Step 24 — assuming ADMIN_USER, ADMIN_EMAIL, ADMIN_PASSWORD are set in the current shell
if ! wp user get "$ADMIN_USER" >/dev/null 2>&1; then
wp user create "$ADMIN_USER" "$ADMIN_EMAIL" \
--role=administrator \
--user_pass="$ADMIN_PASSWORD"
fi
wp user set-role "$ADMIN_USER" administrator
```
If `wp user create` works without the explicit `--user_pass` flag (Shape A in use), prefer that form so the password never appears on the command line.
## Synchronize from remote site
Synchronizing from a remote site means pulling production/staging state into the local site. Keep remote actions read-only: export/read/copy from remote is okay; writing to or mutating remote is not.
Typical flow:
1. Resolve local metadata with `references/wp-project.md` and remote metadata with `references/websimple-gitea.md` Actions variables, especially `WP_SITE_URL`.
2. Confirm source remote URL/host and destination local path/database.
3. Create/reset the local database only after confirmation.
4. Detect the remote/source DB table prefix before export/import. Use `wp_` only if the source also uses `wp_` or the source prefix is unknown and no imported DB is involved.
5. Export the remote database in a read-only way, preferably streaming to local instead of leaving files on the remote host.
6. Import into local DB.
7. Ensure local `wp-config.php` uses the same table prefix as the imported DB.
8. Search-replace remote URLs to the derived local URL.
9. Pull uploads/media from remote unless specified otherwise.
10. Verify `siteurl` and `home` locally.
Preferred DB sync path: stream a compressed remote DB dump over SSH into the local MySQL client. Use this when both remote WP-CLI and local `mysql` are available:
```bash
ssh "$REMOTE_SSH" 'cd "$REMOTE_PATH" && wp db export - --skip-plugins --skip-themes --single-transaction --quick --lock-tables=false | gzip -c' \
| gunzip \
| mysql "$LOCAL_DB_NAME"
```
After DB import, run URL replacement locally:
```bash
wp search-replace "$REMOTE_SITE_URL" "${WEBSIMPLE_STACK_PROTOCOL}://${slug}.${WEBSIMPLE_STACK_DOMAIN}" --all-tables --skip-columns=guid
wp option get siteurl
wp option get home
```
Do not run `wp search-replace` on the remote site during local synchronization.
Preferred uploads sync path: use `rsync` over SSH and include `--delete` so local `wp-content/uploads/` matches remote exactly, unless the user asks to preserve extra local files. Websimple SSH usually requires an explicit port, so include the SSH transport option when needed:
```bash
rsync -az --delete -e "ssh -p $REMOTE_SSH_PORT" "$REMOTE_SSH:$REMOTE_PATH/wp-content/uploads/" "wp-content/uploads/"
```
If the SSH host alias already encodes the port in `~/.ssh/config`, the `-e "ssh -p ..."` option may be omitted. If remote uploads path differs, inspect the project before syncing. Be explicit when `--delete` will remove local-only upload files.
## Provision from existing project
When provisioning from an existing local or remote project:
1. Resolve local and remote metadata using `references/wp-project.md` and `references/websimple-gitea.md`.
2. Confirm the source and destination clearly.
3. Keep remote actions read-only.
4. Import database/content only after confirming the target local DB and path.
5. After import, replace remote URLs with the derived local site URL.
6. Run small verification checks, such as `wp option get siteurl` and `wp option get home`.
## Delete/reset local site
A local delete/reset workflow may include:
- local project directory `${WP_LOCAL_ROOT_PATH}/${slug}`
- local database `wp_${slug}`
- generated/cache artifacts
Before deleting:
1. Show the resolved path, DB name, and local URL.
2. Check whether the path exists and whether it has uncommitted Git changes.
3. Ask for confirmation.
4. Prefer backup/trash for files where practical.
5. Drop the local DB only after explicit confirmation.
Example DB drop command after confirmation:
```bash
mysql -e 'DROP DATABASE `wp_example`;'
```
Use the real derived DB name; quote identifiers safely.

199
references/wp-lovable.md Normal file
View File

@@ -0,0 +1,199 @@
# WP Lovable
Use this skill when turning a Lovable export into a Websimple WordPress project, especially when the output should become a normal WordPress marketing site rather than a detached static React app.
Use related references as needed:
- `references/wp-project.md` and `references/wp-local-site.md` for resolving, creating, provisioning, or checking local WordPress sites.
- `references/websimple-stack.md` for local stack protocol/domain configuration and Traefik SSL routing.
- `references/wp-kaliroots.md` for Kaliroots child-theme structure and template wrapper decisions.
- `references/wp-theme-dev.md` for general theme PHP organization.
- `references/wp-assets.md` for frontend source, Vite/Tailwind, build output, and enqueue integration.
- `references/wp-acf.md` for editable content models, local JSON, options pages, and section data.
- `references/wp-browser.md` for frontend/wp-admin smoke tests and visual checks.
## Core Approach
Keep WordPress as the application shell and source of truth. Treat the Lovable project as frontend source material to port into the active theme, not as a static app to drop into WordPress unchanged.
For new Websimple WordPress projects, start from the `wp-boilerplate` template unless the user or project context says otherwise:
```text
https://gitea.websimple.com/templates/wp-boilerplate
```
Prefer this shape for a Websimple WordPress conversion:
```text
wp-content/themes/{child-theme}/
acf-json/
includes/
core/
assets.php
theme-setup.php
vendors/
acf.php
gravityforms.php
templates/
src/
components/
hooks/
lib/
pages/
main.tsx
index.css
dist/
package.json
vite.config.ts
tailwind.config.*
postcss.config.*
```
Adapt the paths to the existing theme. Do not reorganize established projects broadly without approval.
## First Pass
Before editing, inspect both sides:
1. Identify the Lovable source project, its framework, routes, components, CSS/Tailwind setup, static assets, forms, and data assumptions.
2. Identify the WordPress site, active theme, parent theme, template wrappers, ACF setup, menus, SEO plugin, forms plugin, and asset pipeline.
3. Check git status in the WordPress project and avoid touching unrelated dirty files.
4. Decide which Lovable pieces are presentation components and which are actually content, navigation, SEO, media, forms, or settings that belong in WordPress.
Use `rg --files`, `package.json`, `vite.config.*`, `tailwind.config.*`, `src/`, `includes/`, `templates/`, and `acf-json/` as the main inspection points.
## Porting Rules
Port reusable React/Tailwind UI into the child theme `src/`. Keep TypeScript path aliases, component names, and Tailwind conventions only when they still make sense inside the theme.
Move editable site concerns into WordPress:
- page and section content into pages, ACF fields, flexible content, or options pages;
- navigation into WordPress menus;
- images and downloadable media into the media library or ACF media fields;
- SEO title, canonical, OpenGraph/Twitter, schema, and indexing behavior into the SEO plugin and WordPress-rendered document head;
- contact forms into Gravity Forms or the existing WordPress form plugin;
- global settings such as phone, address, social links, CTAs, and layout toggles into ACF options when they are truly site-wide.
React should render serialized WordPress data with safe fallbacks. It should not become the CMS.
## Routing And SEO
Avoid pure React Router SPA behavior for normal WordPress marketing pages.
Prefer WordPress-rendered URLs and document navigation so SEO plugins can output correct per-page titles, canonical URLs, OpenGraph/Twitter tags, JSON-LD, breadcrumbs, and indexing controls.
Use React routing only when the requested result is an actual app-like interface where client-side routing is intentional. For standard pages, replace internal SPA links with normal document links and let WordPress resolve the route.
## Bundler Pattern
For modern Lovable migrations, Vite in the child theme is usually the cleanest fit:
- keep the app entry at `src/main.tsx` or the existing theme entrypoint;
- keep Tailwind/PostCSS config in the theme root;
- use an alias such as `@` to point at `src` only if the source already uses it or it improves the port;
- output production assets to `dist/`;
- generate `dist/.vite/manifest.json`;
- set production `base` to `/wp-content/themes/{child-theme}/dist/` when assets are served from the theme;
- have PHP enqueue code read the manifest rather than hardcoding hashed filenames;
- mark Vite client and app scripts as `type="module"` when needed.
For local development, bind Vite to the local WordPress host on a fixed port and expose CORS headers so the WordPress page can load the dev server assets.
Mirror Mooncake's local SSL pattern when the Websimple stack uses HTTPS:
```ts
import basicSsl from "@vitejs/plugin-basic-ssl";
import { homedir } from "os";
import { resolve } from "path";
const isHttps = process.env.LOCAL_PROTOCOL === "https";
export default defineConfig({
plugins: [
...(isHttps ? [basicSsl({ certDir: resolve(homedir(), ".local/share/certs") })] : []),
],
server: {
host: `${resolve(__dirname, "../../..").split("/").pop()}.${process.env.TLD || "ledevsimple.ca"}`,
port: 3001,
strictPort: true,
headers: { "Access-Control-Allow-Origin": "*" },
},
});
```
Resolve `LOCAL_PROTOCOL` and `TLD` from the local stack configuration whenever possible:
- `LOCAL_PROTOCOL` should match `$WEBSIMPLE_STACK_PROTOCOL`, usually `https` when Traefik serves local sites with TLS.
- `TLD` should match `$WEBSIMPLE_STACK_DOMAIN`, for example `ledevsimple.ca`.
- Prefer package scripts, shell environment, or generated local env files that pass those values through to Vite.
- If the stack protocol cannot be discovered, inspect `references/websimple-stack.md` config and the current local site URL before choosing `http` or `https`.
Do not let leftover `dist/.vite/manifest.json` silently force production mode if the local workflow expects the dev server. Prefer an explicit local/dev switch, an environment constant, or a detection strategy that matches the project's generated-output policy.
## PHP Integration
Keep PHP integration small and explicit:
- enqueue assets from `includes/core/assets.php` or the existing asset module;
- localize or inline only the WordPress data React needs to render;
- keep data preparation in helpers/includes when it becomes non-trivial;
- keep template files responsible for mounting the frontend and providing page context;
- keep ACF and vendor integrations in the existing vendor/module files.
For Kaliroots child themes, use Kaliroots wrapper and template conventions instead of inventing top-level WordPress templates.
## Forms
Do not rebuild normal WordPress forms in React just because the Lovable export included form markup.
Prefer a WordPress-native form flow:
1. Add an ACF field for a form selector or section configuration.
2. Render the selected Gravity Form or existing form plugin output in PHP or through a focused React wrapper fed by WordPress data.
3. Style the generated form in theme source CSS.
4. Verify submit behavior, validation, notifications, spam protection, and accessibility in the browser.
Only build a custom React form when the project specifically needs custom app behavior and the backend handling is clear.
## What To Avoid
- Do not drop the Lovable export into WordPress as a static page dump.
- Do not make WordPress serve a one-route SPA for normal marketing content.
- Do not hard-code editor-controlled content, layout settings, form IDs, media, menus, or SEO metadata in React.
- Do not edit compiled `dist/` assets manually when source exists.
- Do not migrate build tools, package managers, or theme architecture as a side effect unless the user approved that scope.
- Do not assume Lovable's placeholder copy, mock data, or generated routes are production information.
- Do not use manifest existence alone as the only dev/prod signal if generated output may be checked in or left over locally.
## Verification
Use the smallest checks that cover the migration risk:
```bash
pnpm build
find wp-content/themes/{child-theme} -name '*.php' -print0 | xargs -0 -n1 php -l
```
Then smoke-test representative URLs in the browser:
- home page;
- at least one inner page;
- contact/form page;
- mobile viewport;
- browser console;
- SEO/head output for title, canonical, OpenGraph/Twitter, JSON-LD, and no accidental SPA-only metadata.
Report changed source files, generated files, existing dirty files left untouched, and any checks that could not run.
## Reference Pattern
The first known conversion following this pattern was:
```text
Lovable source: https://github.com/Moonthewhite/chemineeschaleurs
WordPress site: https://gitea.websimple.com/wp-sites/chemineeschaleurs
Child theme: wp-content/themes/cheminees
```
That project used a Kaliroots child theme, React/Tailwind source under `src/`, Vite output under `dist/`, ACF local JSON under `acf-json/`, PHP enqueueing under `includes/core/assets.php`, WordPress/ACF-owned section layout settings, and Gravity Forms for the contact form.

56
references/wp-project.md Normal file
View File

@@ -0,0 +1,56 @@
# WP Project
Use this reference for WordPress-specific Websimple project conventions. For host environment, Docker, Traefik, protocol, or domain assumptions, see `websimple-stack.md`.
## Config values used here
These environment variables drive WordPress project value derivation:
- `WP_LOCAL_ROOT_PATH`: shared local WordPress root path served by `wp-apache2`.
- `WEBSIMPLE_STACK_PROTOCOL`: stack-level protocol used to build local WordPress URLs.
- `WEBSIMPLE_STACK_DOMAIN`: stack-level domain suffix used to build local WordPress URLs.
Read these from the environment. Do not hard-code them in commands or examples; if any are missing when needed, ask the user to export them before proceeding.
## Slug
A Websimple WordPress project is identified by a unique slug derived from its production domain.
Example:
- `www.example.com``example`
## Local WordPress site
For a WordPress project `${slug}`:
- Local project path: `${WP_LOCAL_ROOT_PATH}/${slug}`. In the current websimple-stack this normally maps to `${DOCKER_DATA}/wp/sites/${slug}` on the host and `/var/www/html/${slug}` in containers.
- Local database name: `wp_${slug}`
- Local site URL: `${WEBSIMPLE_STACK_PROTOCOL}://${slug}.${WEBSIMPLE_STACK_DOMAIN}`
- Git repository URL: `https://gitea.websimple.com/wp-sites/${slug}`
## Remote WordPress site
Use the corresponding Gitea repository Actions variables as the source of truth for the remote production environment:
- `WP_SITE_URL`: production site URL source of truth, including protocol.
- `REMOTE_HOST`
- `REMOTE_PORT`
- `REMOTE_USER`
- `REMOTE_PATH`
`wp-sites` repositories may rely on organization/repository fallback variables or secrets for `REMOTE_*` deployment values. If a per-repository `REMOTE_HOST`, `REMOTE_PORT`, `REMOTE_USER`, or `REMOTE_PATH` variable is missing, check the relevant fallback variable/secret source before treating the project metadata as incomplete or attempting to infer SSH values from the public site URL.
Prefer `WP_SITE_URL` for the production site URL. Reading it from repository variables is cheaper and avoids unnecessary remote SSH work. `WP_SITE_URL` must include the protocol, for example `https://www.example.com`.
Normally, the local user has SSH access to `${REMOTE_USER}@${REMOTE_HOST}` on `${REMOTE_PORT}` after repository values and fallback variables/secrets are resolved.
Only perform read-only actions on the remote host. Do not modify files, run updates, change options, write database data, deploy code, or execute destructive commands unless a future workflow explicitly adds a reviewed approval path.
If `WP_SITE_URL` is missing, determine the production site URL from the remote WordPress environment by running WP-CLI read-only from `${REMOTE_PATH}`:
```bash
ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_HOST}" 'cd "${REMOTE_PATH}" && wp option get siteurl'
```
Treat that `siteurl` value as the fallback production site URL. It should include the protocol.

228
references/wp-theme-dev.md Normal file
View File

@@ -0,0 +1,228 @@
# WP Theme Dev
Use this skill for general Websimple WordPress theme PHP development conventions.
Do not use this skill for PHPCS tooling setup or formatting commands; use `references/wp-code-style.md`. Use `references/wp-browser.md` for visual/frontend checks and `references/wp-xdebug.md` for runtime PHP debugging.
## Scope boundaries
This skill covers:
- theme PHP file/folder organization;
- `functions.php` organization;
- actions and filters;
- function naming and namespacing/prefixing;
- template conventions for regular WordPress themes;
- comments/docblocks style;
- escaping, sanitization, translation, and preferred output syntax;
- safe PHP/theme refactoring workflow.
Defer specialized areas to dedicated skills when they apply:
- Kaliroots child themes: use/refine the `references/wp-kaliroots.md` conventions instead of this skill's template guidance.
- PHPCS/PHPCBF/tooling: use `references/wp-code-style.md`.
- ACF architecture/conventions: use `references/wp-acf.md`.
- Asset pipeline details, bundler config, and build commands: use `references/wp-assets.md`.
- Vue/React/headless conventions beyond source placement: use dedicated skills when available.
## Theme structure
Prefer small, discoverable theme files over large catch-all files.
- Keep all PHP logic under `includes/`, grouped by category.
- Keep `functions.php` as a categorized bootstrap/loader for `includes/` files.
- Use category directories such as `includes/admin/`, `includes/core/`, `includes/cpt/`, `includes/taxonomies/`, `includes/forms/`, `includes/roles/`, `includes/schemas/`, `includes/sections/`, `includes/shortcodes/`, `includes/vendors/`, `includes/widgets/`, and `includes/woocommerce/` as applicable.
- Keep helpers/utilities separate from hook callback registration when practical.
- Use `src/` for CSS/SCSS, JavaScript, TypeScript, Vue, React, and similar source assets that are compiled or bundled.
- Preserve the existing project structure unless the user asks for reorganization.
- Do not introduce a new architecture into an established theme without approval.
Preferred `functions.php` pattern: keep it as a categorized loader for files under `includes/`.
```php
<?php
// Administration
require_once __DIR__ . '/includes/admin/attachment.php';
require_once __DIR__ . '/includes/admin/user.php';
// Core
require_once __DIR__ . '/includes/core/assets.php';
require_once __DIR__ . '/includes/core/helpers.php';
require_once __DIR__ . '/includes/core/theme-setup.php';
// Custom Post Types
require_once __DIR__ . '/includes/cpt/project.php';
// Custom Taxonomies
require_once __DIR__ . '/includes/taxonomies/project-category.php';
// Roles
require_once __DIR__ . '/includes/roles/editor.php';
// Shortcodes
require_once __DIR__ . '/includes/shortcodes/alert.php';
// Vendors
require_once __DIR__ . '/includes/vendors/acf.php';
require_once __DIR__ . '/includes/vendors/gravityforms.php';
require_once __DIR__ . '/includes/vendors/polylang.php';
require_once __DIR__ . '/includes/vendors/tinymce.php';
// Widgets
require_once __DIR__ . '/includes/widgets/contact-info.php';
// WooCommerce
require_once __DIR__ . '/includes/woocommerce/cart.php';
require_once __DIR__ . '/includes/woocommerce/checkout.php';
require_once __DIR__ . '/includes/woocommerce/product.php';
```
Keep empty category headers when they make the intended structure clearer or match the project convention.
Avoid modifying WordPress core, vendor directories, uploads, caches, compiled assets, or unrelated themes.
## Templates and template parts
For themes that are child themes of Kaliroots, do not apply generic template conventions. Refer to `references/wp-kaliroots.md` instead.
For regular WordPress themes, templates should live under `templates/` and be grouped by rendering purpose:
```text
templates/
html/ # outer high-level templates
site/ # global site header/footer and site-wide layout parts
content/ # content-related templates
loops/ # loop wrappers and loop item templates
emails/ # outgoing email templates
```
Each template group may have a `partials/` subdirectory for smaller local partials.
Use `templates/content/loops/` for loop templates. Prefer paired names where the outer loop and item template share a base name:
```text
templates/content/loops/events.php
templates/content/loops/events-item.php
templates/content/loops/posts.php
templates/content/loops/posts-item.php
```
Additional conventions:
- Use WordPress template hierarchy intentionally.
- Keep template files readable and focused on rendering.
- Use template parts for repeated markup or meaningful sections.
- Prefer clear names that describe the rendered section or component.
- Keep heavy data preparation, custom queries, and complex conditionals out of templates when a helper or setup function would make the template clearer.
- Keep markup/output close to templates and reusable data/logic close to `includes/` helpers/modules.
Do not mass-reorganize template files without explicit approval.
## Actions and filters
Register hooks in predictable locations, following the theme's existing organization.
- Prefer named callbacks over anonymous closures when the callback is reused, non-trivial, or likely to need debugging.
- Use clear project/theme-prefixed or namespaced callback names.
- Keep callback names tied to the hook purpose.
- Avoid hidden side effects in filters unless intentional and documented by context.
- Preserve important priorities and accepted-argument counts.
- Document or call out non-obvious hook ordering dependencies.
Example pattern:
```php
// Enqueue theme assets.
add_action( 'wp_enqueue_scripts', 'example_enqueue_assets' );
function example_enqueue_assets(): void {
wp_enqueue_style(
'example-theme',
get_stylesheet_directory_uri() . '/dist/css/theme.css',
[],
wp_get_theme()->get( 'Version' )
);
}
// Add a body class on the front page.
add_filter( 'body_class', 'example_add_body_classes' );
function example_add_body_classes( array $classes ): array {
if ( is_front_page() ) {
$classes[] = 'is-front-page';
}
return $classes;
}
```
When refactoring hooks, verify behavior with the smallest meaningful check: WP-CLI, browser smoke test, or Xdebug when needed.
## Function naming
Follow the existing theme convention first. When adding new global functions, avoid generic names.
Prefer one of:
- a theme/project prefix, e.g. `example_get_hero_title()`;
- a namespace if the theme already uses namespaces;
- a class/static method only when the theme already uses that style or it genuinely improves organization.
Keep helper names descriptive. Separate hook callbacks from pure/data helpers when it improves readability.
## Comments and docblocks
Keep the project's existing commenting style coherent.
Comments may be explicit and simple, including human-readable descriptions of what a function does. Do not remove comments just because they restate the function name if that style is consistent in the project.
Use comments/docblocks to improve scanability and clarify intent, inputs, outputs, side effects, or hook context when useful. Avoid stale, misleading, or decorative comments.
## Escaping, sanitization, translation, and output syntax
Escape dynamic values at output time unless WordPress or the data source has already produced safe markup for that exact context.
Common choices:
- `esc_html()` for plain text;
- `esc_attr()` for attribute values;
- `esc_url()` for URLs;
- `wp_kses_post()` for trusted post-like HTML;
- `wp_json_encode()` for JSON output.
Sanitize input before saving or using it in queries. Use WordPress translation helpers for user-facing theme strings when appropriate.
Prefer compact PHP output tags for template output:
```php
<?= esc_html( $title ); ?>
<?= esc_url( $url ); ?>
<?= wp_kses_post( $content ); ?>
```
Do not rewrite existing `<?php echo ...; ?>` output solely for style unless the user asked for cleanup or the surrounding file is already being touched.
## Data and query conventions
- Use core WordPress APIs where possible.
- Reset post data after custom `WP_Query` loops.
- Avoid relying on surprising global state.
- Avoid expensive queries inside repeated template parts.
- Add caching only when justified by a concrete performance issue.
## Refactoring workflow
1. Inspect the theme structure, active child/parent theme relationship, and existing conventions.
2. If the theme is a Kaliroots child theme, defer template decisions to Kaliroots-specific conventions.
3. Make small coherent changes that preserve behavior unless the user requested redesign.
4. Avoid mixing formatting-only and behavior changes.
5. Run lint/style checks via `references/wp-code-style.md` when PHP files changed.
6. Use `references/wp-browser.md` for frontend smoke checks when templates/output changed.
7. Use `references/wp-xdebug.md` when runtime behavior is unclear.
8. Inspect the diff before claiming success.
## Safety
- Do not mass-reorganize a theme without explicit approval.
- Do not touch core, vendor, uploads, caches, compiled assets, or unrelated themes.
- Do not silently force conventions when the existing project clearly uses a different coherent pattern; call out the tradeoff.
- Keep changes reversible and scoped to the user's request.

140
references/wp-xdebug.md Normal file
View File

@@ -0,0 +1,140 @@
# WP Xdebug
Use this skill when the agent should debug local Websimple WordPress PHP execution autonomously. Prefer `references/wp-project.md` first when you need to resolve the site slug, local URL, or project path. Use `references/wp-browser.md` only when browser interaction is needed to understand or reproduce the behavior.
The expected path is autonomous debugging with `koriym/xdebug-mcp`; do not require the user to have VS Code open or listening for Xdebug connections.
## Scope and safety
- Use Xdebug only for local Websimple development sites unless the user explicitly confirms otherwise.
- Do not install tools, enable/reconfigure Xdebug, restart services, or modify project files unless requested.
- Avoid sending public/production form submissions or external-effect requests while debugging.
- Prefer deterministic local reproductions: PHP script, WP-CLI command, PHPUnit command, safe curl/API request, or a minimal reproducer.
- If autonomous runtime debugging cannot be performed because `xdebug-mcp` or Xdebug is missing, report that as a blocker and suggest the smallest next setup step.
## Required autonomous debugger
Use `koriym/xdebug-mcp` to provide Xdebug-backed tools with structured JSON output for AI analysis:
- `xtrace`: trace execution flow.
- `xstep`: stop at breakpoints and inspect variables.
- `xprofile`: collect performance profiling data.
- `xcoverage`: collect coverage data.
- `xback`: capture call stack/backtrace at a breakpoint.
- `xcompare`: compare variable states across runs; CLI-only.
Check availability before relying on it:
```bash
command -v xtrace xstep xprofile xcoverage xback
```
If Composer global binaries are not on `PATH`, locate them with:
```bash
composer global config bin-dir --absolute --quiet
```
Installation, only when explicitly requested:
```bash
composer global require koriym/xdebug-mcp
```
## Preconditions
Before triggering a debug run, confirm or infer:
- The target is local code/site, normally `${WEBSIMPLE_STACK_PROTOCOL}://${slug}.${WEBSIMPLE_STACK_DOMAIN}` and `${WP_LOCAL_ROOT_PATH}/${slug}`.
- Xdebug 3.x is installed for the PHP runtime used by the debug command.
- `xdebug-mcp` CLI tools are available, or MCP exposes the equivalent tools.
- The reproduction command actually executes the PHP code being investigated.
For Websimple WordPress, remember that the served document root inside the PHP container is normally `/var/www/html`; map that mentally to the local WordPress project root when interpreting file paths.
## Autonomous workflow
1. Resolve the project slug, local URL, and local path with `references/wp-project.md` conventions.
2. Identify the code path and the safest reproduction strategy:
- WP-CLI command when WordPress bootstrap/state is needed.
- PHP script or small throwaway reproducer for isolated functions/classes.
- PHPUnit or project test command when a test exists.
- curl/API request only when it safely and actually triggers the PHP code under debug.
- Browser only to discover state, cookies, nonces, or the user flow; then reduce to a deterministic command when possible.
3. Pick the xdebug-mcp tool:
- Need execution path? Use `xtrace`.
- Need variable state at a line? Use `xstep --break='file.php:line'`.
- Need caller chain? Use `xback --break='file.php:line'`.
- Need slowness data? Use `xprofile --json`.
- Need coverage? Use `xcoverage`.
4. Run the smallest safe command that captures runtime data.
5. Analyze the JSON output and inspect source files as needed.
6. Iterate with narrower breakpoints/traces until the cause is clear.
7. Report evidence: command shape, breakpoint/trace target, observed runtime data, conclusion, and any blockers.
## Example commands
Script or isolated command:
```bash
xtrace -- php path/to/script.php
xstep --break='path/to/file.php:42' -- php path/to/script.php
xback --break='path/to/file.php:42' -- php path/to/script.php
xprofile --json -- php path/to/script.php
```
WP-CLI reproduction from the local WordPress root. When using the `/usr/local/bin/wp` PHAR, invoke it explicitly through PHP so `xdebug-mcp` can determine the target script:
```bash
cd "${WP_LOCAL_ROOT_PATH}/${slug}"
xtrace -- php /usr/local/bin/wp --path="${WP_LOCAL_ROOT_PATH}/${slug}" eval '/* minimal safe reproduction */'
xstep --break='wp-content/themes/theme/file.php:42' -- php /usr/local/bin/wp --path="${WP_LOCAL_ROOT_PATH}/${slug}" eval '/* minimal safe reproduction */'
xback --break='wp-content/themes/theme/file.php:42' -- php /usr/local/bin/wp --path="${WP_LOCAL_ROOT_PATH}/${slug}" eval '/* minimal safe reproduction */'
```
Test reproduction:
```bash
xtrace -- vendor/bin/phpunit --filter 'RelevantTest'
xcoverage -- vendor/bin/phpunit --filter 'RelevantTest'
```
Containerized reproduction, if the PHP code must run inside the Websimple stack, should use the actual service names after inspection. Standard PHP is `wp-php`; browser-triggered Xdebug requests are routed to `wp-php-xdebug` when the `XDEBUG_SESSION=vscode` cookie is present.
```bash
xtrace -- docker compose exec -T wp-php php /var/www/html/${slug}/path/to/script.php
xstep --break='/var/www/html/${slug}/wp-content/themes/theme/file.php:42' -- docker compose exec -T wp-php php /var/www/html/${slug}/path/to/script.php
```
Only use command forms that match the actual Websimple stack project layout after inspecting the project/stack.
## Browser and HTTP flows
For browser-only bugs:
- Use `references/wp-browser.md` to reproduce the UI and inspect the request details.
- Avoid relying on a human IDE listener.
- Prefer converting the observed request into a safe deterministic command: WP-CLI, script, test, or curl with required cookies/nonces.
- Be careful: wrapping `curl` with `xtrace` only debugs PHP if the Xdebug capture is attached to the PHP runtime handling the request. If the PHP executes in Apache/FPM separately, use a supported xdebug-mcp/container strategy or reduce the behavior to WP-CLI/script instead.
Do not use Xdebug triggers on production/remote URLs.
## Common WordPress targets
- Frontend template load: theme template, `template_redirect`, query hooks, block rendering.
- wp-admin page: menu page callbacks, list tables, metaboxes, save handlers.
- REST API: route callback and permission callback.
- admin-ajax: `wp_ajax_*` / `wp_ajax_nopriv_*` handlers.
- Forms: validation/submission hooks, but do not submit externally-effectful forms unless explicitly requested.
- Cron-like handlers: only trigger explicitly requested local code paths; do not run destructive jobs by accident.
## Troubleshooting
If no runtime data appears:
- Confirm `xtrace`/`xstep`/`xback` is installed and on `PATH`.
- Confirm the command after `--` actually runs PHP and reaches the code path.
- Confirm Xdebug 3.x is installed for that PHP binary/container.
- Confirm paths in breakpoints match the runtime paths shown in traces, often `/var/www/html/...` inside containers.
- Confirm the request/command is local, not production.
- If browser navigation is blocked by policy, use `references/wp-browser.md`s browser SSRF prerequisite for `${WEBSIMPLE_STACK_DOMAIN}` local sites.

219
scripts/gitea.sh Normal file
View File

@@ -0,0 +1,219 @@
#!/usr/bin/env bash
# Websimple Gitea API wrapper — read-only operations.
#
# Replaces the OpenClaw plugin's three Gitea tools:
# - websimple_gitea_orgs_repos_list
# - websimple_gitea_repos_actions_variables_list
# - websimple_gitea_repos_actions_variables_get
#
# Requires:
# - jq
# - curl
# - WEBSIMPLE_GITEA_API_TOKEN env var (or --token flag)
#
# Usage:
# gitea.sh repos-list [--org wp-sites]
# gitea.sh vars-list <owner> <repo>
# gitea.sh var-get <owner> <repo> <variable-name>
#
# Output: JSON to stdout. Errors to stderr. Exit non-zero on failure.
set -euo pipefail
GITEA_BASE_URL="https://gitea.websimple.com"
GITEA_API_BASE="${GITEA_BASE_URL}/api/v1"
PAGE_LIMIT=100
# ----- helpers -----
die() {
printf 'gitea.sh: %s\n' "$*" >&2
exit 1
}
require_token() {
if [[ -z "${WEBSIMPLE_GITEA_API_TOKEN:-}" ]]; then
die "WEBSIMPLE_GITEA_API_TOKEN is not set. Export it before calling this script."
fi
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
# Issue an authenticated GET. Echos response body, exits non-zero on HTTP error.
# Args: $1 = path beginning with /
gitea_get() {
local path="$1"
local url="${GITEA_API_BASE}${path}"
local response
local http_code
response="$(curl -sS \
--header "Authorization: token ${WEBSIMPLE_GITEA_API_TOKEN}" \
--header "Accept: application/json" \
--write-out "\n__HTTP_CODE__:%{http_code}" \
"${url}")"
http_code="${response##*__HTTP_CODE__:}"
response="${response%$'\n'__HTTP_CODE__:*}"
if [[ "${http_code}" -lt 200 || "${http_code}" -ge 300 ]]; then
die "Gitea API request failed: HTTP ${http_code} for ${url}"
fi
printf '%s' "${response}"
}
# Fetch all pages for an endpoint that supports ?page=&limit=.
# Args: $1 = base path (without query string); $2 = extra query params (may be empty)
# Echoes a single JSON array combining all pages.
gitea_get_paginated() {
local base_path="$1"
local extra_query="${2:-}"
local page=1
local combined='[]'
local page_json
local page_count
while (( page <= 10000 )); do
local sep="?"
[[ -n "${extra_query}" ]] && sep="?${extra_query}&"
local path="${base_path}${sep}page=${page}&limit=${PAGE_LIMIT}"
# If gitea_get fails (HTTP error), the subshell exits non-zero and the
# caller's `|| exit 1` handles propagation.
page_json="$(gitea_get "${path}")" || return 1
# Defensive: empty/null responses become []
if [[ -z "${page_json}" || "${page_json}" == "null" ]]; then
page_json='[]'
fi
combined="$(jq -c --argjson new "${page_json}" '. + $new' <<<"${combined}")"
page_count="$(jq 'length' <<<"${page_json}")"
if (( page_count < PAGE_LIMIT )); then
printf '%s' "${combined}"
return 0
fi
page=$(( page + 1 ))
done
die "Pagination safety limit exceeded for ${base_path}"
}
# ----- subcommands -----
cmd_repos_list() {
local org="wp-sites"
while (( $# > 0 )); do
case "$1" in
--org) org="$2"; shift 2 ;;
--org=*) org="${1#--org=}"; shift ;;
*) die "Unknown argument to repos-list: $1" ;;
esac
done
require_token
local repos
repos="$(gitea_get_paginated "/orgs/${org}/repos" "")" || exit 1
# Match the OpenClaw tool's output shape: filtered fields + metadata envelope.
jq -n \
--arg baseUrl "${GITEA_BASE_URL}" \
--arg org "${org}" \
--argjson pageSize "${PAGE_LIMIT}" \
--argjson repos "${repos}" \
'{
baseUrl: $baseUrl,
org: $org,
fetchedAll: true,
pageSize: $pageSize,
count: ($repos | length),
repos: ($repos
| sort_by((.full_name // .name // ""))
| map({
id, name, full_name, html_url, clone_url, ssh_url,
default_branch, private, archived, updated_at
}))
}'
}
cmd_vars_list() {
(( $# == 2 )) || die "Usage: gitea.sh vars-list <owner> <repo>"
local owner="$1" repo="$2"
require_token
local variables
variables="$(gitea_get_paginated "/repos/${owner}/${repo}/actions/variables" "")" || exit 1
jq -n \
--arg baseUrl "${GITEA_BASE_URL}" \
--arg owner "${owner}" \
--arg repo "${repo}" \
--argjson pageSize "${PAGE_LIMIT}" \
--argjson variables "${variables}" \
'{
baseUrl: $baseUrl,
owner: $owner,
repo: $repo,
fetchedAll: true,
pageSize: $pageSize,
count: ($variables | length),
variables: ($variables | sort_by(.name // ""))
}'
}
cmd_var_get() {
(( $# == 3 )) || die "Usage: gitea.sh var-get <owner> <repo> <variable-name>"
local owner="$1" repo="$2" name="$3"
require_token
local variable
variable="$(gitea_get "/repos/${owner}/${repo}/actions/variables/${name}")" || exit 1
jq -n \
--arg baseUrl "${GITEA_BASE_URL}" \
--arg owner "${owner}" \
--arg repo "${repo}" \
--argjson variable "${variable}" \
'{
baseUrl: $baseUrl,
owner: $owner,
repo: $repo,
variable: $variable
}'
}
usage() {
cat >&2 <<'EOF'
Websimple Gitea API wrapper — read-only.
Usage:
gitea.sh repos-list [--org <org>] (default org: wp-sites)
gitea.sh vars-list <owner> <repo>
gitea.sh var-get <owner> <repo> <variable-name>
Requires WEBSIMPLE_GITEA_API_TOKEN in the environment.
EOF
exit 2
}
# ----- dispatch -----
require_cmd jq
require_cmd curl
[[ $# -ge 1 ]] || usage
cmd="$1"; shift
case "${cmd}" in
repos-list) cmd_repos_list "$@" ;;
vars-list) cmd_vars_list "$@" ;;
var-get) cmd_var_get "$@" ;;
-h|--help|help) usage ;;
*) die "Unknown subcommand: ${cmd}" ;;
esac