From 82149a1da341c7a54c7cc2e6b06e0c27a9ebf4ac Mon Sep 17 00:00:00 2001 From: Aryan Jasala Date: Fri, 2 Oct 2026 04:11:27 +0530 Subject: [PATCH 1/2] chore(init): remove the Dev Tools feature for now Drop bin/features/dev-tools.js, its tests, the end-to-end script and its docs, its entry in bin/scaffold.config.js, and the mentions in AGENTS.md, the init skill and prompt, and the docs. rtcamp/wp-dev-tools is not public yet, so the toggle only worked with repository access. --- .claude/skills/init/SKILL.md | 13 +- .github/prompts/init.prompt.md | 11 +- AGENTS.md | 1 - bin/dev-tools-e2e.sh | 309 -------------------------- bin/features/dev-tools.js | 281 ------------------------ bin/scaffold.config.js | 6 - docs/dev-tools-demo.md | 206 ------------------ docs/features.md | 1 - docs/index.md | 3 +- docs/initialization.md | 5 +- docs/internal/README.md | 1 - docs/internal/dev-tools-e2e.md | 112 ---------- docs/internal/maintenance.md | 4 +- tests/js/scaffold-config.test.js | 358 ------------------------------- 14 files changed, 16 insertions(+), 1295 deletions(-) delete mode 100755 bin/dev-tools-e2e.sh delete mode 100644 bin/features/dev-tools.js delete mode 100644 docs/dev-tools-demo.md delete mode 100644 docs/internal/dev-tools-e2e.md delete mode 100644 tests/js/scaffold-config.test.js diff --git a/.claude/skills/init/SKILL.md b/.claude/skills/init/SKILL.md index c937c5b..d70a589 100644 --- a/.claude/skills/init/SKILL.md +++ b/.claude/skills/init/SKILL.md @@ -46,7 +46,7 @@ Then verify a clean working tree (`git status`) - init rewrites files irreversib ### 3. Ask, confirm, run - one step `npm run init` is a single command; don't turn gathering its inputs into a multi-round-trip wizard. -**Setup:** in ONE message ask for project name (required, e.g. `Acme Blog` → namespace `Acme_Blog\Features`, package `rtcamp/acme-blog-features`, text domain, prefixes, main file - show these back), version (default `1.0.0`), which capability sets to remove, and which features to enable (defaults: keep all sets, `hmr` on, `tailwind` and `dev-tools` off - see Capability model). The engine derives the tokens itself; do not read the engine source (`identity.js` etc.) - the mapping above is the contract, and the graph answers any deeper question (see the graphify policy in `AGENTS.md`). +**Setup:** in ONE message ask for project name (required, e.g. `Acme Blog` → namespace `Acme_Blog\Features`, package `rtcamp/acme-blog-features`, text domain, prefixes, main file - show these back), version (default `1.0.0`), which capability sets to remove, and which features to enable (defaults: keep all sets, `hmr` on, `tailwind` off - see Capability model). The engine derives the tokens itself; do not read the engine source (`identity.js` etc.) - the mapping above is the contract, and the graph answers any deeper question (see the graphify policy in `AGENTS.md`). **Manage:** ask which of identity / features to change. It's a single flag on an existing project - no wizard needed. @@ -54,10 +54,10 @@ Get explicit consent on the resolved values (init is destructive), then run: ```bash # Setup: npm run init -- --name="Acme Blog" --version=1.0.0 --yes \ - --remove-examples=cron,rest --features=hmr,tailwind,dev-tools + --remove-examples=cron,rest --features=hmr,tailwind # Manage: npm run init -- --list -npm run init -- --enable=dev-tools --yes +npm run init -- --enable=tailwind --yes npm run init -- --features=hmr --yes # exact enabled set (empty = none) ``` - `--keep-examples` keeps all; `--remove-examples` (no value) removes all; `--remove-examples=a,b` removes listed keys. @@ -69,7 +69,6 @@ npm run init -- --features=hmr --yes # exact enabled set (empty = none) ### 4. After init - Tailwind enabled → developer runs `npm install` (it added `src/css/tailwind.css`, `postcss.config.js`, and pinned `@rtcamp/tailwind-config`). -- Dev Tools enabled → it's still a private package (`rtcamp/wp-dev-tools`, VCS-sourced): developer runs `composer update rtcamp/wp-dev-tools`, which may need GitHub auth, then `npm run dev:connect`. - `composer dump-autoload` (engine runs it when `composer.json` is present). - Trust the engine's own output to verify (it reports the removed sets, drops their `Main::CLASSES` lines and tests, and regenerates the autoloader). To confirm a symbol or reference, run a single `graphify query`/`affected` against the graph - never grep `Main.php`/`inc/` to check removal. - If you hand-edited PHP and are NOT handing off to scaffold (e.g. a manage-mode dangling `Main::CLASSES` cleanup), run `composer lint` on the change and fix it. When a brief follows (step 5), leave the lint/PHPStan/test gates to the scaffold skill - init does not run them. @@ -97,9 +96,9 @@ ONE "Select the capabilities to include" prompt. Each is keep-or-remove; removin | APIs & Automation | `rest`, `cli`, `cron` | | Admin | `settings`, `admin`, `roles` | | Utilities | `cache`, `transients` | -| Developer Tooling | `test-measure` (CI workflow), `hmr` (feature), `dev-tools` (feature) | +| Developer Tooling | `test-measure` (CI workflow), `hmr` (feature) | -Sets are kept by default; pass keys to `--remove-examples` to drop. Features: `hmr` on, `tailwind` and `dev-tools` off; toggle via `--features`/`--enable`/`--disable`. `dev-tools` (`rtcamp/wp-dev-tools`, runtime telemetry over MCP - Query Monitor + the MCP Adapter) is the one feature still sourced from a private VCS repo; enabling it needs GitHub auth on `composer update` (see step 4). Everything else here installs from public registries. +Sets are kept by default; pass keys to `--remove-examples` to drop. Features: `hmr` on, `tailwind` off; toggle via `--features`/`--enable`/`--disable`. Everything here installs from public registries. ### Changing capabilities after setup Add later → scaffold skill / `npx wp-tooling add /` (writes the class, wires the module). Remove later → delete its `inc/Modules//` classes + `Main::CLASSES` line by hand. Toggle a feature any time via `npm run init -- --enable=`/`--disable=`. Do not re-run init to change the capability set. @@ -113,6 +112,6 @@ Add later → scaffold skill / `npx wp-tooling add /` (writes th - Never invent flags. Supported: `--name`, `--version`, `--yes`, `--keep-examples`, `--remove-examples[=...]`, `--features`, `--enable`, `--disable`, `--reinit`, `--list`, `--clean`, `--help`. Run `npm run init -- --help` if unsure. ## Reference -- Engine: `@rtcamp/wp-tooling/init` (via `bin/init.js`). Capability map: `bin/scaffold.config.js`. Dev Tools feature: `bin/features/dev-tools.js`. +- Engine: `@rtcamp/wp-tooling/init` (via `bin/init.js`). Capability map: `bin/scaffold.config.js`. - Conventions renamed into: `AGENTS.md`, `.github/instructions/structure.instructions.md`. - Graphify policy: `AGENTS.md`. diff --git a/.github/prompts/init.prompt.md b/.github/prompts/init.prompt.md index efff5a7..e4f438b 100644 --- a/.github/prompts/init.prompt.md +++ b/.github/prompts/init.prompt.md @@ -45,15 +45,15 @@ Then verify a clean working tree (`git status`) - init rewrites files irreversib ### 3. Ask, confirm, run - one step `npm run init` is a single command; don't turn gathering its inputs into a multi-round-trip wizard. -**Setup:** in ONE message ask for project name (required, e.g. `Acme Blog` → namespace `Acme_Blog\Features`, package `rtcamp/acme-blog-features`, text domain, prefixes, main file - show these back), version (default `1.0.0`), which capability sets to remove, and which features to enable (defaults: keep all sets, `hmr` on, `tailwind` and `dev-tools` off - see Capability model). +**Setup:** in ONE message ask for project name (required, e.g. `Acme Blog` → namespace `Acme_Blog\Features`, package `rtcamp/acme-blog-features`, text domain, prefixes, main file - show these back), version (default `1.0.0`), which capability sets to remove, and which features to enable (defaults: keep all sets, `hmr` on, `tailwind` off - see Capability model). **Manage:** ask which of identity / features to change - it's a single flag on an existing project, no wizard needed. Get explicit consent on the resolved values (init is destructive), then run: ```bash -npm run init -- --name="Acme Blog" --version=1.0.0 --yes --remove-examples=cron,rest --features=hmr,tailwind,dev-tools +npm run init -- --name="Acme Blog" --version=1.0.0 --yes --remove-examples=cron,rest --features=hmr,tailwind # Manage: npm run init -- --list -npm run init -- --enable=dev-tools --yes +npm run init -- --enable=tailwind --yes npm run init -- --features=hmr --yes # exact enabled set (empty = none) ``` - `--keep-examples` keeps all; `--remove-examples` (no value) removes all; `--remove-examples=a,b` removes listed keys. @@ -62,7 +62,6 @@ npm run init -- --features=hmr --yes # exact enabled set (empty = none) ### 4. After init - Tailwind enabled → developer runs `npm install` (added `src/css/tailwind.css`, `postcss.config.js`, pinned `@rtcamp/tailwind-config`). -- Dev Tools enabled → still a private package (`rtcamp/wp-dev-tools`, VCS-sourced): developer runs `composer update rtcamp/wp-dev-tools` (may need GitHub auth), then `npm run dev:connect`. - Suggest `composer dump-autoload`. - If you hand-edited PHP under `inc/`, run `composer format` → `composer lint` → `composer phpstan` and resolve every finding. Never silence a real issue; if unclear, STOP and ask. - Refresh the LOCAL knowledge graph with `graphify update .`. Never commit `graphify-out/graph.json`, and never run the cross-repo `/graphify . --update` or `merge-graphs`. If not installed, say so in one line; do not block. See `AGENTS.md` (graphify). @@ -78,9 +77,9 @@ ONE keep-or-remove prompt; removing deletes the capability entirely (classes, `i | APIs & Automation | `rest`, `cli`, `cron` | | Admin | `settings`, `admin`, `roles` | | Utilities | `cache`, `transients` | -| Developer Tooling | `test-measure` (CI workflow), `hmr` (feature), `dev-tools` (feature) | +| Developer Tooling | `test-measure` (CI workflow), `hmr` (feature) | -Sets kept by default; pass keys to `--remove-examples` to drop. Features: `hmr` on, `tailwind` and `dev-tools` off. `dev-tools` is the one feature still sourced from a private VCS repo (see step 4); everything else installs from public registries. +Sets kept by default; pass keys to `--remove-examples` to drop. Features: `hmr` on, `tailwind` off. Everything installs from public registries. ## Hard rules - Package managers only with consent: `npm install`, `composer install` (step 2), and `npm run init`. Otherwise surface install commands as developer actions. diff --git a/AGENTS.md b/AGENTS.md index 68aa646..18eb384 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,6 @@ To add a feature: write the test, create the concrete class extending the right - PHP: `composer lint` (PHPCS) · `composer format` (phpcbf) · `composer phpstan`; the PHPUnit suite runs under `wp-env` via `npm run test:php`. - JS/build: `npm run build:dev` · `npm run build:prod` · `npm run lint` · `npm run test`. - Setup / features: `npm run init` (setup + manage wizard) · `npx wp-tooling add /` (add a feature). -- Dev tools (opt-in): `npm run init -- --enable=dev-tools` then `npm run dev:connect` exposes runtime telemetry over MCP — [demo](docs/dev-tools-demo.md) · [e2e check](docs/internal/dev-tools-e2e.md). Human documentation: [README.md](README.md) → [docs/index.md](docs/index.md) (developer guides), [DEVELOPMENT.md](DEVELOPMENT.md) (extending by hand), [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/internal/](docs/internal/README.md) (skeleton maintainers). Framework API detail is linked, not copied, from the `rtcamp/wp-primitives` docs at the locked tag. diff --git a/bin/dev-tools-e2e.sh b/bin/dev-tools-e2e.sh deleted file mode 100755 index 2cf6ae7..0000000 --- a/bin/dev-tools-e2e.sh +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env bash -# -# dev-tools-e2e.sh - prove the whole wp-dev-tools loop against this project's -# wp-env, from package boot through to a real MCP tool call. -# -# Exit codes: -# 0 every check passed -# 1 one or more checks failed -# 2 a prerequisite is missing (Docker down, wp-env not started, dev-tools -# not wired into this project) -# -# Run it after any wp-dev-tools change, and after `composer update -# rtcamp/wp-dev-tools -W`, to catch a regression before it reaches a demo. -# -# This script only reads. It curls the site and makes read-only WP-CLI and MCP -# calls inside the container. It never installs anything, never writes a file -# and never runs git -- a missing prerequisite is printed as the command for -# you to run, not run for you. -# -set -uo pipefail - -# ---- configuration ----------------------------------------------------------- - -SITE_URL="http://localhost:8888" -SKIP_CWV=0 - -# The allow-list, mirrored from Mcp\ServerRegistrar::ABILITIES. MCP sanitises -# the ability id by replacing "/" with "-", so these are the tool ids a client -# sees, not the ability ids. -EXPECTED_TOOLS="wp-dev-tools-audit-rest-permissions -wp-dev-tools-compare-requests -wp-dev-tools-get-runtime-health -wp-dev-tools-get-telemetry -wp-dev-tools-get-web-vitals -wp-dev-tools-list-requests -wp-dev-tools-profile-url" - -EXPECTED_COUNT=7 - -# ---- output helpers ---------------------------------------------------------- - -log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } -ok() { printf '\033[1;32m ok:\033[0m %s\n' "$*"; } -warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; } -err() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; } - -have() { command -v "$1" >/dev/null 2>&1; } - -# `printf ... | grep -q` is a trap here: grep exits on the first match, the -# producer takes SIGPIPE, and `pipefail` then reports the whole pipeline as -# failed even though the match succeeded. A herestring keeps it one command. -contains() { grep -q "$1" <<<"$2"; } - -FAILURES=0 -fail() { err "$*"; FAILURES=$(( FAILURES + 1 )); } - -usage() { - awk '/^# dev-tools-e2e/,/^set -uo/ { if ($0 !~ /^set -uo/) { sub(/^#[ ]?/, ""); print } }' "$0" - cat <<'USAGE' -Usage: bash bin/dev-tools-e2e.sh [options] - - --url=URL Site URL to probe (default http://localhost:8888) - --skip-cwv Skip the Core Web Vitals beacon check - -h, --help Show this help -USAGE -} - -for arg in "$@"; do - case "$arg" in - --url=*) SITE_URL="${arg#*=}" ;; - --skip-cwv) SKIP_CWV=1 ;; - -h|--help) usage; exit 0 ;; - *) err "Unknown option: $arg"; usage; exit 2 ;; - esac -done - -SITE_URL="${SITE_URL%/}" - -# ---- container helpers ------------------------------------------------------- - -# wp-env prints its own progress chatter on stderr; only stdout is the payload. -wp_cli() { npx wp-env run cli -- wp "$@" 2>/dev/null; } - -# One STDIO MCP session: initialize, announce, then whatever is piped in. -# $1 is the server id; the remaining JSON-RPC lines come from stdin. -mcp_session() { - local server="$1" - { - printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"dev-tools-e2e","version":"1"}}}' - printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}' - cat - } | npx wp-env run cli -- wp mcp-adapter serve --server="$server" --user=admin 2>/dev/null -} - -# ---- prerequisites ----------------------------------------------------------- - -log "wp-dev-tools end-to-end check against ${SITE_URL}" - -have npx || { err "npx not found; install Node 22 (nvm use)."; exit 2; } - -if ! have docker || ! docker info >/dev/null 2>&1; then - err "Docker is not running. Start Docker Desktop, then: npm run wp-env start" - exit 2 -fi - -if ! curl -fsS -o /dev/null --max-time 10 "$SITE_URL/"; then - err "$SITE_URL is not responding. Start the environment:" - err " npm run wp-env start" - exit 2 -fi - -# ---- 1. the package booted, and the gate is open ----------------------------- - -log "1. Package booted" - -BOOTED="$(wp_cli eval 'echo defined( "RT_DEV_TOOLS_BOOTED" ) ? "yes" : "no";')" -case "$BOOTED" in - *yes*) ok "RT_DEV_TOOLS_BOOTED is defined" ;; - *) fail "RT_DEV_TOOLS_BOOTED is not defined. The package boots from the plugin's autoloader, so check the plugin is active first:" - warn " npx wp-env run cli -- wp plugin list" - warn " npx wp-env run cli -- wp plugin activate \$(basename \"\$PWD\")" - warn "and that the package is installed: composer update rtcamp/wp-dev-tools -W" ;; -esac - -# BOOTED only proves the autoload ran inside WordPress. This is the check that -# proves the two hard gates (RT_DEV_TOOLS_DEV_MODE + environment type) passed. -ENABLED="$(wp_cli eval 'echo \rtCamp\WPDevTools\Support\Config::is_enabled() ? "yes" : "no";')" -case "$ENABLED" in - *yes*) ok "the dev gate is open (Config::is_enabled)" ;; - *) fail "the dev gate is closed -- check RT_DEV_TOOLS_DEV_MODE and WP_ENVIRONMENT_TYPE in .wp-env.override.json, then: npm run wp-env start" ;; -esac - -# ---- 2. abilities registered ------------------------------------------------- - -log "2. Abilities registered" - -# The single quotes are deliberate: this is PHP for `wp eval`, and $k/$a must -# reach the container unexpanded. -# shellcheck disable=SC2016 -ABILITIES="$(wp_cli eval 'foreach ( wp_get_abilities() as $k => $a ) { $n = is_object( $a ) && method_exists( $a, "get_name" ) ? $a->get_name() : (string) $k; if ( 0 === strpos( $n, "wp-dev-tools/" ) ) { echo $n, "\n"; } }' | grep '^wp-dev-tools/' | sort)" -ABILITY_COUNT="$(printf '%s\n' "$ABILITIES" | grep -c '^wp-dev-tools/')" - -if [ "$ABILITY_COUNT" -eq "$EXPECTED_COUNT" ]; then - ok "$ABILITY_COUNT abilities registered under wp-dev-tools/" -else - fail "expected $EXPECTED_COUNT abilities under wp-dev-tools/, found $ABILITY_COUNT" - printf '%s\n' "$ABILITIES" | sed 's/^/ /' -fi - -# ---- 3. the dedicated MCP route responds ------------------------------------- - -log "3. Dedicated MCP route" - -ROUTE_CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 -X POST \ - -H 'Content-Type: application/json' \ - --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ - "$SITE_URL/wp-json/wp-dev-tools/mcp")" - -# Only a refusal is a pass: the route exists and is turning away an -# unauthenticated call. Anything else -- including curl's 000 for a timeout or -# a dropped connection -- is a failure. -case "$ROUTE_CODE" in - 401|403) ok "/wp-json/wp-dev-tools/mcp responds (HTTP $ROUTE_CODE)" ;; - 404) fail "/wp-json/wp-dev-tools/mcp returned 404 -- the MCP Adapter plugin is inactive, or the gate is closed" ;; - 000) fail "/wp-json/wp-dev-tools/mcp did not answer within 10s (connection failed or timed out)" ;; - 2*) fail "/wp-json/wp-dev-tools/mcp accepted an unauthenticated call (HTTP $ROUTE_CODE) -- it must refuse one" ;; - *) fail "/wp-json/wp-dev-tools/mcp returned HTTP $ROUTE_CODE, expected 401 or 403" ;; -esac - -# ---- 4. STDIO tools/list is exactly the allow-list ---------------------------- - -log "4. STDIO tools/list" - -TOOLS_RAW="$(printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | mcp_session wp-dev-tools)" -PROTOCOL="$(grep -o '"protocolVersion":"[^"]*"' <<<"$TOOLS_RAW" | head -1 | cut -d'"' -f4)" -TOOLS="$(grep -o '"wp-dev-tools-[a-z-]*"' <<<"$TOOLS_RAW" | tr -d '"' | sort -u)" -TOOL_COUNT="$(printf '%s\n' "$TOOLS" | grep -c '^wp-dev-tools-')" - -if [ -n "$PROTOCOL" ]; then - ok "negotiated MCP protocol version $PROTOCOL" -else - fail "no protocolVersion in the initialize response -- the STDIO session did not start" -fi - -if [ "$TOOL_COUNT" -eq "$EXPECTED_COUNT" ] && [ "$TOOLS" = "$EXPECTED_TOOLS" ]; then - ok "tools/list is exactly the $EXPECTED_COUNT-tool allow-list" -else - fail "tools/list does not match the allow-list (found $TOOL_COUNT)" - diff <(printf '%s\n' "$EXPECTED_TOOLS") <(printf '%s\n' "$TOOLS") | sed 's/^/ /' -fi - -# ---- 5. the capture loop ----------------------------------------------------- - -log "5. Capture loop" - -# Capture is on shutdown, and WP-CLI requests are never captured, so the loop -# needs two real HTTP requests to compare. Each is tagged so it can be picked -# out of the list by URL; the pause keeps their second-resolution captured_at -# values apart. -curl -s -o /dev/null --max-time 10 "$SITE_URL/?dev-tools-e2e=before" -sleep 1 -curl -s -o /dev/null --max-time 10 "$SITE_URL/?dev-tools-e2e=after" - -LIST="$(printf '%s\n' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"wp-dev-tools-list-requests","arguments":{"limit":10}}}' | mcp_session wp-dev-tools)" - -# Decode the rows rather than scraping UUIDs out of the raw JSON, whose text -# order says nothing about which capture came first. Prints " " for -# the newest capture of each tagged request, newest first by captured_at. -PICKED="$(node -e ' - let raw = ""; - process.stdin.on("data", (c) => (raw += c)).on("end", () => { - for (const line of raw.split("\n")) { - let msg; - try { msg = JSON.parse(line); } catch { continue; } - if (msg.id !== 3 || !msg.result) continue; - let data = msg.result.structuredContent; - if (!data) { - try { data = JSON.parse(msg.result.content[0].text); } catch { data = {}; } - } - const rows = (data.requests || []) - .filter((r) => r && r.id && r.captured_at) - .sort((a, b) => Date.parse(b.captured_at) - Date.parse(a.captured_at)); - for (const tag of ["after", "before"]) { - const row = rows.find((r) => String(r.url).includes("dev-tools-e2e=" + tag)); - if (row) console.log(tag + " " + row.id); - } - } - }); -' <<<"$LIST")" - -AFTER="$(awk '$1 == "after" { print $2; exit }' <<<"$PICKED")" -BEFORE="$(awk '$1 == "before" { print $2; exit }' <<<"$PICKED")" - -if [ -n "$BEFORE" ] && [ -n "$AFTER" ]; then - ok "list-requests returned both tagged captures" -else - fail "list-requests is missing the tagged captures (before: ${BEFORE:-none}, after: ${AFTER:-none}) -- is Query Monitor active? (curl once and retry)" -fi - -if [ -n "$AFTER" ]; then - TELEMETRY="$(printf '%s\n' "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"wp-dev-tools-get-telemetry\",\"arguments\":{\"request_id\":\"$AFTER\"}}}" | mcp_session wp-dev-tools)" - if contains '"db_queries"' "$TELEMETRY" && contains '"metrics"' "$TELEMETRY"; then - ok "get-telemetry returned the normalised sections" - else - fail "get-telemetry did not return db_queries + metrics for $AFTER" - fi - - # host_file is null for core-owned frames by design; at least one frame from - # this project's own code must translate, or the root pair is wrong. - if contains '"host_file":"/' "$TELEMETRY"; then - ok "host file:line translation is working" - else - warn "no translated host_file in this capture -- fine if nothing in your own code ran a query, wrong CONTAINER_ROOT/HOST_ROOT otherwise" - fi -fi - -if [ -n "$BEFORE" ] && [ -n "$AFTER" ]; then - COMPARE="$(printf '%s\n' "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{\"name\":\"wp-dev-tools-compare-requests\",\"arguments\":{\"before_request_id\":\"$BEFORE\",\"after_request_id\":\"$AFTER\"}}}" | mcp_session wp-dev-tools)" - if contains '"deltas"' "$COMPARE"; then - ok "compare-requests returned deltas" - else - fail "compare-requests did not return deltas for $BEFORE -> $AFTER" - fi -fi - -# ---- 6. the CWV beacon is enqueued on the front end -------------------------- - -if [ "$SKIP_CWV" -eq 1 ]; then - log "6. CWV beacon (skipped)" -else - log "6. CWV beacon" - HTML="$(curl -s "$SITE_URL/")" - if contains 'rt-dev-tools-cwv-beacon' "$HTML" && contains 'rtDevToolsCwv' "$HTML"; then - ok "the beacon script and its config are enqueued on the front end" - else - fail "the CWV beacon is not on the front end -- it is skipped in wp-admin, so check you probed a front-end URL" - fi -fi - -# ---- 7. the default adapter server exposes none of ours ---------------------- - -log "7. Default adapter server isolation" - -DEFAULT_TOOLS="$(printf '%s\n' '{"jsonrpc":"2.0","id":6,"method":"tools/list"}' | mcp_session mcp-adapter-default-server)" -LEAKED="$(grep -o '"wp-dev-tools-[a-z-]*"' <<<"$DEFAULT_TOOLS" | sort -u | tr -d '"')" - -if [ -z "$LEAKED" ]; then - ok "the default server exposes no wp-dev-tools-* tools" -else - fail "the default server is leaking tools -- abilities must not be flagged meta.mcp.public:" - printf '%s\n' "$LEAKED" | sed 's/^/ /' -fi - -if contains 'discover-abilities' "$DEFAULT_TOOLS"; then - ok "the default server is up (discover-abilities present), so the check is meaningful" -else - warn "the default server listed no tools at all -- the isolation check above proves less than it looks" -fi - -# ---- summary ----------------------------------------------------------------- - -echo -if [ "$FAILURES" -eq 0 ]; then - log "All checks passed." - exit 0 -fi - -log "$FAILURES check(s) failed." -exit 1 diff --git a/bin/features/dev-tools.js b/bin/features/dev-tools.js deleted file mode 100644 index bd7cf5d..0000000 --- a/bin/features/dev-tools.js +++ /dev/null @@ -1,281 +0,0 @@ -/** - * WP Dev Tools feature, declared for the @rtcamp/wp-tooling init engine. - * - * Writes committed manifest wiring (composer.json, package.json scripts) plus - * local dev-environment state (.wp-env.override.json, gitignored). The - * committed .wp-env.json is never touched, so enabling this never forces Query - * Monitor and the MCP Adapter onto teammates. - */ - -/** - * External dependencies - */ -const path = require( 'path' ); - -// Repo is rtCamp/wp-devtools, package is rtcamp/wp-dev-tools -- both correct. -const PACKAGE = 'rtcamp/wp-dev-tools'; -// No `main` branch upstream. Once wp-devtools is published, this and REPOSITORY -// give way to a plain version constraint. -const CONSTRAINT = 'dev-release/v1.0.0'; -const REPOSITORY = { - type: 'vcs', - url: 'https://github.com/rtCamp/wp-devtools.git', - 'no-api': true, -}; - -// Query Monitor collects; the MCP Adapter exposes. wp-env plugins, not deps. -// Pinned deliberately -- a floating dev environment would drift under every -// generated project. Bump here when a newer adapter release is adopted. -const PLUGINS = [ - 'https://downloads.wordpress.org/plugin/query-monitor.zip', - 'https://github.com/WordPress/mcp-adapter/releases/download/v0.5.0/mcp-adapter.zip', -]; - -const COMPOSER_FILE = 'composer.json'; -const GITIGNORE_FILE = '.gitignore'; -const WP_ENV_FILE = '.wp-env.json'; -const WP_ENV_OVERRIDE = '.wp-env.override.json'; - -// The override half `detect` keys off. Shared with the config it writes, so a -// rename cannot leave detect silently reporting "disabled". -const DEV_MODE_KEY = 'RT_DEV_TOOLS_DEV_MODE'; - -// `--user=admin`: every ability is capability-checked and fails anonymously. -const CONNECT_SCRIPT = - 'claude mcp add wp-dev-tools -- npx wp-env run cli -- wp mcp-adapter serve --server=wp-dev-tools --user=admin'; -const DISCONNECT_SCRIPT = 'claude mcp remove wp-dev-tools'; - -// `{}` for a missing file, null for malformed -- callers decide which matters. -const readJson = ( api, rel ) => { - try { - return JSON.parse( api.read( rel ) || '{}' ); - } catch { - return null; - } -}; - -// Containers stop carrying their own weight once emptied; drop them so disable -// leaves no hollow `"require-dev": {}` behind. -const deleteIfEmpty = ( parent, key ) => { - if ( parent[ key ] && ! Object.keys( parent[ key ] ).length ) { - delete parent[ key ]; - } -}; - -// wp-env mounts "." as the host folder name, not the text domain. -const containerRoot = ( api ) => { - const bucket = 'theme' === api.identity.kind ? 'themes' : 'plugins'; - return `/var/www/html/wp-content/${ bucket }/${ path.basename( - api.root - ) }`; -}; - -// wp-env replaces the plugins array rather than merging it, so the override has -// to re-emit whatever .wp-env.json already lists. -const basePlugins = ( api ) => { - const wpEnv = readJson( api, WP_ENV_FILE ) || {}; - const development = ( wpEnv.env && wpEnv.env.development ) || {}; - if ( Array.isArray( development.plugins ) ) { - return development.plugins; - } - if ( Array.isArray( wpEnv.plugins ) ) { - return wpEnv.plugins; - } - return 'theme' === api.identity.kind ? [] : [ '.' ]; -}; - -// Override entries that are neither committed nor ours: a developer's own. -const developerPlugins = ( api, development ) => { - const base = basePlugins( api ); - const listed = Array.isArray( development.plugins ) - ? development.plugins - : []; - return listed.filter( - ( url ) => ! base.includes( url ) && ! PLUGINS.includes( url ) - ); -}; - -// SAVEQUERIES is not part of the gate -- it is what gives Query Monitor the -// backtraces behind file:line. A wrong root pair makes every host_file null. -const overrideConfig = ( api ) => ( { - WP_ENVIRONMENT_TYPE: 'local', - SAVEQUERIES: true, - [ DEV_MODE_KEY ]: true, - RT_DEV_TOOLS_TELEMETRY_CONTAINER_ROOT: containerRoot( api ), - RT_DEV_TOOLS_TELEMETRY_HOST_ROOT: api.root, - RT_DEV_TOOLS_TELEMETRY_LOOPBACK_BASE: 'http://wordpress', -} ); - -// Defensive, for forks that trimmed their .gitignore: the override holds -// absolute host paths and must never be committed. -const ensureGitignored = ( api ) => { - const current = api.read( GITIGNORE_FILE ); - if ( null === current ) { - api.write( GITIGNORE_FILE, `/${ WP_ENV_OVERRIDE }\n` ); - return; - } - const listed = current - .split( '\n' ) - .some( - ( line ) => line.trim().replace( /^\//, '' ) === WP_ENV_OVERRIDE - ); - if ( listed ) { - return; - } - const prefix = current.endsWith( '\n' ) ? current : `${ current }\n`; - api.write( GITIGNORE_FILE, `${ prefix }/${ WP_ENV_OVERRIDE }\n` ); -}; - -const addComposerEntry = ( api ) => { - api.editJson( COMPOSER_FILE, ( json ) => { - const repositories = json.repositories || []; - const hasRepository = repositories.some( - ( entry ) => entry.url === REPOSITORY.url - ); - if ( ! hasRepository ) { - repositories.push( { ...REPOSITORY } ); - } - json.repositories = repositories; - json[ 'require-dev' ] = { - ...json[ 'require-dev' ], - [ PACKAGE ]: CONSTRAINT, - }; - } ); -}; - -const removeComposerEntry = ( api ) => { - api.editJson( COMPOSER_FILE, ( json ) => { - if ( json[ 'require-dev' ] ) { - delete json[ 'require-dev' ][ PACKAGE ]; - deleteIfEmpty( json, 'require-dev' ); - } - if ( Array.isArray( json.repositories ) ) { - json.repositories = json.repositories.filter( - ( entry ) => entry.url !== REPOSITORY.url - ); - deleteIfEmpty( json, 'repositories' ); - } - } ); -}; - -const writeOverride = ( api ) => { - api.editJson( - WP_ENV_OVERRIDE, - ( json ) => { - json.env = json.env || {}; - const development = json.env.development || {}; - // Rebuilt from the current committed list, so a .wp-env.json that - // gained a plugin is picked up rather than masked. - development.plugins = [ - ...new Set( [ - ...basePlugins( api ), - ...developerPlugins( api, development ), - ...PLUGINS, - ] ), - ]; - development.config = { - ...development.config, - ...overrideConfig( api ), - }; - json.env.development = development; - }, - { create: true, indentFrom: WP_ENV_FILE } - ); -}; - -// wp-env replaces this array, so anything left here pins the dev env. Drop it -// when only the committed list remains; a developer's own entry can be kept -// only by restating the base beside it. -const prunePlugins = ( api, development ) => { - const kept = developerPlugins( api, development ); - if ( ! kept.length ) { - delete development.plugins; - return; - } - development.plugins = [ ...new Set( [ ...basePlugins( api ), ...kept ] ) ]; -}; - -// Prune ours, then drop the file only if nothing else is holding it up. -const pruneOverride = ( api ) => { - if ( ! api.exists( WP_ENV_OVERRIDE ) ) { - return; - } - - api.editJson( WP_ENV_OVERRIDE, ( json ) => { - const development = ( json.env && json.env.development ) || null; - if ( ! development ) { - return; - } - if ( development.config ) { - Object.keys( overrideConfig( api ) ).forEach( - ( key ) => delete development.config[ key ] - ); - deleteIfEmpty( development, 'config' ); - } - if ( Array.isArray( development.plugins ) ) { - prunePlugins( api, development ); - } - deleteIfEmpty( json.env, 'development' ); - deleteIfEmpty( json, 'env' ); - } ); - - const remaining = readJson( api, WP_ENV_OVERRIDE ) || {}; - if ( ! Object.keys( remaining ).length ) { - api.remove( WP_ENV_OVERRIDE ); - } -}; - -const onEnable = ( api ) => { - addComposerEntry( api ); - writeOverride( api ); - ensureGitignored( api ); - - // `update`, not `install`: the lock has no entry for a just-added package. - api.note( - `wp-dev-tools: run \`composer update ${ PACKAGE } -W\`, then \`npm run wp-env start && npm run dev:connect\`.` - ); - api.note( - `wp-dev-tools: ${ WP_ENV_OVERRIDE } carries this machine's paths and is gitignored -- teammates run \`npm run init -- --enable=dev-tools\` to wire their own.` - ); -}; - -const onDisable = ( api ) => { - removeComposerEntry( api ); - api.note( - `wp-dev-tools: run \`composer update ${ PACKAGE } -W\` to drop it from composer.lock and vendor/.` - ); - pruneOverride( api ); -}; - -// Both halves required: the composer entry alone would read "enabled" on a -// clone where the gitignored override is missing and nothing actually works. -// Total by contract -- it also runs read-only under `init --list`. -const detect = ( api ) => { - const composer = readJson( api, COMPOSER_FILE ); - const override = readJson( api, WP_ENV_OVERRIDE ); - if ( ! composer || ! override ) { - return false; - } - const development = ( override.env && override.env.development ) || {}; - return ( - Boolean( ( composer[ 'require-dev' ] || {} )[ PACKAGE ] ) && - true === ( development.config || {} )[ DEV_MODE_KEY ] - ); -}; - -module.exports = { - key: 'dev-tools', - label: 'WP Dev Tools (runtime telemetry over MCP)', - category: 'Developer Tooling', - description: - 'Opt-in. Adds rtcamp/wp-dev-tools as a dev dependency and the dev:connect script, and writes the local wp-env override (Query Monitor + the MCP Adapter, the dev-mode gate, and the host path map). The committed .wp-env.json is left untouched.', - apply: { - scripts: { - 'dev:connect': CONNECT_SCRIPT, - 'dev:disconnect': DISCONNECT_SCRIPT, - }, - }, - onEnable, - onDisable, - detect, -}; diff --git a/bin/scaffold.config.js b/bin/scaffold.config.js index 3142530..bcff7dc 100644 --- a/bin/scaffold.config.js +++ b/bin/scaffold.config.js @@ -19,11 +19,6 @@ * `features` instead, so the engine can add/remove those deps on toggle. */ -/** - * Internal dependencies - */ -const devTools = require( './features/dev-tools' ); - const mainFile = ( api ) => `${ api.identity.package.split( '/' )[ 1 ] }.php`; const tailwindConst = ( api ) => `${ api.identity.constantPrefix }_FEATURES_ENABLE_TAILWIND`; @@ -164,7 +159,6 @@ module.exports = { ); }, }, - devTools, ], // Keep/remove capabilities. Removing deletes the module + classes + its diff --git a/docs/dev-tools-demo.md b/docs/dev-tools-demo.md deleted file mode 100644 index 4f011ab..0000000 --- a/docs/dev-tools-demo.md +++ /dev/null @@ -1,206 +0,0 @@ -# WP Dev Tools demo — seven beats - -A copy-paste tour of [wp-dev-tools](https://github.com/rtCamp/wp-devtools) on this project: seven prompts that walk the whole loop, from "what can you even see?" to an agent diagnosing a slow page from live runtime data and pointing at the line of code responsible. It takes about ten minutes on a fresh environment. - -The point of the package is that it gives a coding agent things it *cannot* read from source: which queries actually ran, how long they actually took, what the browser actually measured. Every beat below is a prompt you type into Claude Code, not a command you run — the shell lines are only there to generate traffic. - -> Beats 1–5 and 7 need nothing but the setup below. **Beat 6 needs a real browser visit** — `curl` cannot produce Core Web Vitals. Read that beat before you start if you want the browser visit to line up with the rest. - -## Setup - -```bash -# 1. Enable the feature. Writes composer.json, the dev:connect scripts, and the -# gitignored .wp-env.override.json carrying this machine's paths. -npm run init -- --enable=dev-tools - -# 2. Resolve the package. `update`, not `install`: the lock has no entry yet. -composer update rtcamp/wp-dev-tools -W - -# 3. Boot. wp-env installs Query Monitor and the MCP Adapter from the override. -npm run wp-env start - -# 4. Register the MCP server with Claude Code. Local scope; nothing is committed. -npm run dev:connect -``` - -`/mcp` inside Claude Code should now show `wp-dev-tools` connected with **7 tools**. `npm run dev:disconnect` undoes step 4. - -Confirm the plugin itself is active — the package boots from its Composer autoloader, so an inactive plugin means nothing registers: - -```bash -npx wp-env run cli -- wp plugin activate "$(basename "$PWD")" -``` - -Three of the settings step 1 writes are worth knowing, because each one has a distinctive failure: - -| Setting | What it buys you | If it is wrong | -|---|---|---| -| `SAVEQUERIES` | Query Monitor's query backtraces | telemetry has no `file`/`line` at all | -| `CONTAINER_ROOT` / `HOST_ROOT` | the prefix map turning `/var/www/html/…` into a path your editor can open | every `host_file` comes back `null` | -| `LOOPBACK_BASE` | an in-container base URL, since `localhost:8888` is unreachable from inside | `profile-url` fails to capture | - -### Give it something to find - -The demo is more convincing with a real problem in it, so add one. Paste this at the bottom of your main plugin file — it is deliberately throwaway, and deleting it *is* beat 5: - -```php -// --- TEMPORARY: wp-dev-tools demo. Delete this block in beat 5. --- -add_action( - 'wp', - static function (): void { - if ( is_admin() ) { - return; - } - global $wpdb; - for ( $i = 0; $i < 25; $i++ ) { - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->get_var( 'SELECT SLEEP(0.01)' ); - } - } -); -``` - -It lives in your own code on purpose. Telemetry only translates container paths that sit under `CONTAINER_ROOT` — your plugin directory — so a slow query raised from *your* file is one the agent can point you straight at. The same block dropped into `wp-content/mu-plugins/` would report `host_file: null` and the best part of the demo would be lost. The `phpcs:ignore` line keeps `npm run lint:php` clean while the block is in place, so nothing else in your workflow trips over it. - -## Beat 1 — What can you see? - -``` -What wp-dev-tools tools do you have available? List them with one line each on what data they surface. -``` - -Expect all seven: `list-requests`, `get-telemetry`, `compare-requests`, `profile-url`, `get-web-vitals`, `get-runtime-health`, `audit-rest-permissions`. If the list is empty, `/mcp` will show whether the server is actually connected. - -## Beat 2 — What just happened? - -Generate some traffic first — capture runs on `shutdown`, and WP-CLI requests are never captured: - -```bash -curl -s -o /dev/null http://localhost:8888/ -``` - -``` -Use list-requests to show the last few captured requests with their headline metrics. -``` - -You get one row per request, with `id`, `url`, `type`, `status`, `captured_at` and the headline numbers. The slow block is already visible in the aggregate — 50 queries instead of 25, and two orders of magnitude more time in the database: - -```text -captured_at type status total queries query time url -2026-08-18T12:12:03+00:00 frontend 200 539.00ms 50 365.23ms http://localhost:8888/ -2026-08-18T12:09:59+00:00 frontend 200 64.50ms 25 3.14ms http://localhost:8888/ -``` - -## Beat 3 — Why was it slow? - -``` -Take the most recent capture and show me its slowest database query — the SQL, how long it took, and the host file:line I can open. -``` - -This is the beat that justifies the whole package. The answer names a real file on your machine: - -```text -16.19ms SELECT SLEEP(0.01) - caller Acme_Analytics\Features\{closure}() - host_file /path/to/acme-analytics/acme-analytics-features.php:89 -``` - -In that capture, 25 of the 50 query frames carried a translated `host_file` and 25 came back `null`. **That is correct, not a bug** — the null ones are core-owned frames outside `CONTAINER_ROOT`, and the translator returns `null` rather than inventing a path. - -`get-telemetry` returns far more than queries: `db_queries`, `http`, `php_errors`, `assets`, `doing_it_wrong`, `cache`, `hooks` and a `metrics` summary. Worth a follow-up prompt: - -``` -Anything else in that capture worth worrying about? Check the cache hit rate, the doing_it_wrong notices and the PHP errors. -``` - -## Beat 4 — Is it actually consistent? - -One capture could be a cold cache or a noisy neighbour. `profile-url` issues its own loopback requests and reports medians, so it works even with zero prior traffic: - -``` -Profile http://localhost:8888/ with 5 samples and give me the median timings. -``` - -```text -batch 8ff46e21-d503-4648-8629-f4c5d72b892e -samples 5 × HTTP 200 -medians total 470.60ms · queries 50 · query time 355.48ms -``` - -Five for five, so it is the code, not the weather. - -## Beat 5 — Prove the fix - -Now delete the temporary block you added during setup, and capture the same URL again: - -```bash -curl -s -o /dev/null http://localhost:8888/ -``` - -``` -Compare the capture from before I removed that block with the one after, and tell me exactly what improved. -``` - -```text -total_time_ms 467.5 -> 104.1 -363.4 -77.7% -query_count 50 -> 25 -25 -50% -query_time_ms 312.74 -> 4.57 -308.17 -98.5% -peak_memory_mb 7.8 -> 7.7 -0.1 -1.3% -php_error_count 0 -> 0 0 null -http_call_count 0 -> 0 0 null -``` - -That is the loop closing: a change was made, and the runtime data proves what it did. (`delta_pct` is `null` where the before value was zero — there is no percentage change from nothing.) - -## Beat 6 — What did the browser see? - -Server timings say nothing about what a user experiences. Field Core Web Vitals come from a real browser, and only from a real browser: - -1. Open `http://localhost:8888/` in Chrome. -2. Leave the tab **focused for about seven seconds** — the beacon's idle flush fires at six. -3. Switch to another tab or close it. That triggers the authoritative flush that finalises CLS and INP. - -Click something on the page while you are there, or INP will have nothing to report. - -``` -Show the Core Web Vitals for the homepage, and correlate them with the server telemetry for the same request. -``` - -You get the field metrics with their attribution (what element was the LCP, what shifted, what the input delay was), a p75 aggregate across recent samples, and a correlation block tying them back to the server-side capture — duplicate queries, slow queries, error counts. - -Skip any of the three steps and the tool tells you so rather than inventing numbers: - -```text -No Core Web Vitals captured for http://localhost:8888/ yet. Open it in a real -browser (Chrome); field metrics are beaconed when the tab is hidden or after a -short idle. Then retry. -``` - -If you want the vitals and the server telemetry to describe the same regression, do this browser visit **before** you delete the block in beat 5. - -## Beat 7 — The finale - -Everything so far named a tool. This one does not: - -``` -The homepage feels slow. Find out why from runtime data, and point me at the code. -``` - -A connected agent should work it out on its own: list the recent captures, pull the telemetry for the slowest, sort the queries, and come back with the file and line. It may reach for `profile-url` to confirm the pattern repeats, or `get-runtime-health` to rule out overdue cron and heavy autoloaded options. Put the temporary block back first if you want it to have something to find. - -This is the whole pitch in one prompt: no tool names, no dashboard, no guessing from source — the agent reads what actually happened. - -## Hygiene notes - -- **`list-requests` came back empty.** Nothing has been requested yet. Capture runs on `shutdown`, and WP-CLI requests are never captured — `curl` the site once and retry. -- **Every ability errors.** The dev gate is shut. Check `RT_DEV_TOOLS_DEV_MODE` and `WP_ENVIRONMENT_TYPE` in `.wp-env.override.json`, then `npm run wp-env start` — constants only reach `wp-config.php` on a restart. -- **Every `host_file` is `null`.** The `CONTAINER_ROOT`/`HOST_ROOT` pair is wrong. Re-run `npm run init -- --enable=dev-tools`; it derives both from the current checkout. -- **Some `host_file`s are `null`.** Expected. Only paths under `CONTAINER_ROOT` translate. -- **Old captures vanish.** By design. The store is a ring buffer, pruned on write and hourly, against both a count cap (200 captures) and a TTL (7 days). -- **`curl` produced no Core Web Vitals.** It never will, and neither will `profile-url`. Beat 6 needs a browser. -- **Nothing is exposed in production.** The package is a `require-dev` dependency, and it registers nothing unless `RT_DEV_TOOLS_DEV_MODE` is truthy *and* the environment type is `local`. - -## Related - -- [Initialization](initialization.md#optional-dependencies) — enabling and disabling the feature. -- [internal/dev-tools-e2e.md](internal/dev-tools-e2e.md) — the maintainers' repeatable check that all of this still works. -- [wp-devtools package guide](https://github.com/rtCamp/wp-devtools/blob/release/v1.0.0/README.md) — configuration and the full tool reference (repository access required). diff --git a/docs/features.md b/docs/features.md index e3f535b..98eacd3 100644 --- a/docs/features.md +++ b/docs/features.md @@ -62,4 +62,3 @@ Unlike example sets, these can be turned on and off at any time with `npm run in | --- | --- | --- | --- | --- | | `hmr` | `webpack.config.js`, `webpack.blocks.config.js`, `inc/Core/Assets.php`, `.env.local` | On | BrowserSync live reload for assets and PHP; Fast Refresh for block editor components. Toggling only flips `ENABLE_HMR`; dependencies stay installed. | [Live reload](hmr.md) | | `tailwind` | `bin/features/tailwind/`, `inc/Core/Assets.php`, main plugin file | Off | Adds `src/css/tailwind.css` and `postcss.config.js`, declares Tailwind dependencies, and sets `ACME_CONTENT_FEATURES_ENABLE_TAILWIND` to `true` so the stylesheet is enqueued. | [Tailwind](tailwind.md) | -| `dev-tools` | `bin/features/dev-tools.js` | Off | Adds `rtcamp/wp-dev-tools` (private) as a dev dependency, `dev:connect` / `dev:disconnect` scripts, and a gitignored `.wp-env.override.json` with Query Monitor and the MCP Adapter. A coding agent can then read live request telemetry. | [Dev Tools demo](dev-tools-demo.md) | diff --git a/docs/index.md b/docs/index.md index 625b2a9..e147ba1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,6 @@ The skeleton is a consumer of shared rtCamp libraries. It owns the plugin's stru | [`rtcamp/wp-primitives`](https://github.com/rtCamp/wp-primitives/blob/v2.0.0/docs/index.md) | `Registrable` + `Loader` + `Container`, the `Abstract*` base classes, asset/component/template loaders, `Cache`, `Transients`, `Encryptor`, `Logger` | Framework docs (linked from each guide) | | [`@rtcamp/wp-tooling`](https://github.com/rtCamp/wp-tooling/blob/main/node-packages/wp-tooling/README.md) | `npm run init` engine, `npx wp-tooling add` scaffolds, shared ESLint/Stylelint configs, release scripts | wp-tooling docs | | [`rtCamp/wp-shared-workflows`](https://github.com/rtCamp/wp-shared-workflows) | Reusable lint, test and build workflows | wp-shared-workflows docs | -| [wp-devtools](https://github.com/rtCamp/wp-devtools/blob/release/v1.0.0/README.md) (optional) | Runtime telemetry over MCP | wp-devtools docs (repository access required) | ## Two ways to work @@ -35,7 +34,7 @@ Both are valid: pick the CLI for full control over every input, or the AI skill ## Further reading -- [Live reload and block HMR](hmr.md), [Tailwind](tailwind.md), and the [Dev Tools demo](dev-tools-demo.md) cover the optional development features. +- [Live reload and block HMR](hmr.md) and [Tailwind](tailwind.md) cover the optional development features. - [Contributing](../CONTRIBUTING.md) and the [maintainer guidance](internal/README.md) are for developers working on the skeleton itself. ## Command reference diff --git a/docs/initialization.md b/docs/initialization.md index 92c0689..9f45f70 100644 --- a/docs/initialization.md +++ b/docs/initialization.md @@ -60,7 +60,7 @@ npm run init -- --name="Acme Content" --version=1.0.0 --yes \ | APIs & Automation | `rest`, `cli`, `cron` | | | Admin | `settings`, `admin`, `roles` | | | Utilities | `cache`, `transients` | | -| Developer Tooling | `test-measure` (the CI caller workflow) | `hmr`, `dev-tools` | +| Developer Tooling | `test-measure` (the CI caller workflow) | `hmr` | What each set contains is listed in [Included features](features.md). @@ -72,7 +72,7 @@ What each set contains is listed in [Included features](features.md). | Version | Written to the main file's header and `package.json`. | | Removed example sets | The module file (`inc/Modules/.php`), its folder, its test (`tests/php/Test.php`), its `Main::CLASSES` line and any coupled region (for example the static block list in `inc/Core/Assets.php` for `blocks`, the unschedule call in `inc/Core/PluginSetup.php` for `cron`). | | Kept example sets | Stay as working references. Their `// wp:example:` markers are removed. | -| Optional features | `hmr` edits `.env.local`; `tailwind` adds `src/css/tailwind.css`, `postcss.config.js`, dependencies and flips the Tailwind constant; `dev-tools` adds Composer and npm script wiring plus a gitignored `.wp-env.override.json`. | +| Optional features | `hmr` edits `.env.local`; `tailwind` adds `src/css/tailwind.css`, `postcss.config.js`, dependencies and flips the Tailwind constant. | | State | `.wp-scaffold.json` records identity and feature choices. Do not edit it by hand. | | Cleanup | Removes `tests/js/scaffold-config.test.js`, which only tests the skeleton's own init config. Copilot files, workflows and templates under `.github/` stay. | | AI instructions | `sync-ai` refreshes `.github/instructions/framework-php.instructions.md` from the installed framework. | @@ -109,7 +109,6 @@ npm run init -- --help # options for your installed engine - **HMR:** toggling only flips `ENABLE_HMR` in `.env.local`, a per-developer file. See [Live reload](hmr.md). - **Tailwind:** enabling changes declarations and adds files; run `npm install` afterwards. See [Tailwind](tailwind.md). -- **Dev Tools:** needs access to the private [wp-devtools](https://github.com/rtCamp/wp-devtools) repository (Composer package `rtcamp/wp-dev-tools`). Enable it, then follow init's printed steps: `composer update rtcamp/wp-dev-tools -W`, `npm run wp-env start`, `npm run dev:connect`. The override file holds this machine's paths, so each developer enables it locally. See the [Dev Tools demo](dev-tools-demo.md) and the [package guide](https://github.com/rtCamp/wp-devtools/blob/release/v1.0.0/README.md). ## Troubleshooting diff --git a/docs/internal/README.md b/docs/internal/README.md index 41bc6a9..24d138d 100644 --- a/docs/internal/README.md +++ b/docs/internal/README.md @@ -15,7 +15,6 @@ The [maintenance guide](maintenance.md) covers: Related pages: - [Knowledge graph](knowledge-graph.md): installing graphify, querying the graph, and who refreshes the committed baseline. -- [Dev Tools end-to-end check](dev-tools-e2e.md): the repeatable check for the optional Dev Tools feature. Documentation and validation results must identify the skeleton commit and the installed dependency revisions. The guides target `main` and changes built on it; do not assume a separately installed dependency checkout behaves the same. diff --git a/docs/internal/dev-tools-e2e.md b/docs/internal/dev-tools-e2e.md deleted file mode 100644 index 1a042b1..0000000 --- a/docs/internal/dev-tools-e2e.md +++ /dev/null @@ -1,112 +0,0 @@ -# WP Dev Tools end-to-end check - -A repeatable check that the whole [wp-dev-tools](https://github.com/rtCamp/wp-devtools) loop still works in this project's wp-env — the package booting, the abilities registering, the MCP allow-list, a real tool call that returns telemetry, and the isolation guarantees that keep the tooling out of production. Run it after any wp-dev-tools change, so a regression surfaces here rather than halfway through a demo. - -> The check only reads. It curls the site and makes read-only WP-CLI and MCP calls inside the container; it never installs anything, never writes a file, and never runs `git`. When a prerequisite is missing it prints the command for you to run rather than running it. - -For a guided tour of what these tools actually tell you, see [dev-tools-demo.md](../dev-tools-demo.md). - -## Prerequisites - -- Dev tools enabled on this project: `npm run init -- --enable=dev-tools`, then `composer update rtcamp/wp-dev-tools -W`. -- Docker running, and the environment up: `npm run wp-env start`. -- This plugin **active** in the environment. The package boots from the plugin's Composer autoloader, so an inactive plugin means nothing registers at all. Renaming the project (which `npm run init` does on a first run) renames the main plugin file and can leave WordPress pointing at the old one: - ```bash - npx wp-env run cli -- wp plugin list - npx wp-env run cli -- wp plugin activate "$(basename "$PWD")" - ``` - -## Run it - -```bash -bash bin/dev-tools-e2e.sh -``` - -| Option | Purpose | -|---|---| -| `--url=URL` | Site to probe (default `http://localhost:8888`) | -| `--skip-cwv` | Skip the Core Web Vitals beacon check | -| `-h`, `--help` | Show usage | - -| Exit code | Meaning | -|---|---| -| `0` | Every check passed | -| `1` | One or more checks failed | -| `2` | A prerequisite is missing — Docker down, environment not started, `npx` absent | - -## What each check proves - -| # | Check | Why it is here | -|---|---|---| -| 1 | Package booted, gate open | `RT_DEV_TOOLS_BOOTED` only proves the autoload ran inside WordPress — it is defined **even when the dev gate is shut**. `Support\Config::is_enabled()` is the one that proves both hard gates (`RT_DEV_TOOLS_DEV_MODE` truthy *and* `wp_get_environment_type() === 'local'`) passed. | -| 2 | Abilities registered | All seven abilities are present under the `wp-dev-tools/` prefix via the core Abilities API. Catches a registrar that silently stopped running. | -| 3 | Dedicated MCP route responds | `/wp-json/wp-dev-tools/mcp` exists. Only a `401` or `403` is a **pass** — the route is there and refusing an unauthenticated call. Everything else fails: `404` means the MCP Adapter is inactive or the gate is shut, `000` means the request timed out (10s) or never connected, and a `2xx` means the route accepted an anonymous call it should have refused. | -| 4 | STDIO `tools/list` | The exposed surface is *exactly* the allow-list in `Mcp\ServerRegistrar::ABILITIES` — no more, no fewer. Also records the negotiated MCP protocol version. Note the tool ids are **hyphenated** (`wp-dev-tools-get-telemetry`): MCP sanitises the ability id by replacing `/` with `-`. | -| 5 | Capture loop | The real chain end to end: two tagged HTTP requests (`?dev-tools-e2e=before`, then `?dev-tools-e2e=after`) → `list-requests`, picked out by URL and ordered by `captured_at` → `get-telemetry` → `compare-requests`, all over the same STDIO transport an agent uses. Also confirms container→host `file:line` translation is working. | -| 6 | CWV beacon enqueued | The web-vitals library and the beacon script are on the front end with their config object. This proves only that the beacon *ships* — real field data needs a browser (see the demo doc). The beacon is deliberately skipped in `wp-admin`. | -| 7 | Default adapter server isolation | The MCP Adapter's own default server exposes **none** of our tools. Abilities are never flagged `meta.mcp.public`, so the dedicated server's allow-list is the single reviewable place that decides what a client can see. This is the check that would catch that invariant being weakened. | - -Two results are intentionally *not* failures: - -- **`host_file: null` on core-owned frames is correct.** `Support\PathTranslator` only translates paths under `RT_DEV_TOOLS_TELEMETRY_CONTAINER_ROOT`, and returns `null` rather than fabricating a path. The check warns, rather than fails, when a capture contains no translated frame at all — that is expected if none of your own code ran a query on that request. -- **An empty `list-requests`** just means nothing has been requested yet. Capture runs on `shutdown`, and WP-CLI requests are never captured, so the script issues its own HTTP requests first. - -## Recorded pass - -Recorded 18 August 2026 against the reference consumer — an init-scaffolded clone of this skeleton with `--enable=dev-tools`. - -| | | -|---|---| -| WordPress | 7.2-alpha-63315 (`WordPress/WordPress#master`) | -| PHP | 8.3.33 | -| `rtcamp/wp-dev-tools` | 0.2.0, locked at `dev-release/v1.0.0` (`770bbe5`) | -| Query Monitor | 4.0.7 | -| MCP Adapter | 0.5.0 | -| MCP protocol | 2025-06-18 | -| Tools exposed | 7 | - -```text -==> wp-dev-tools end-to-end check against http://localhost:8888 -==> 1. Package booted - ok: RT_DEV_TOOLS_BOOTED is defined - ok: the dev gate is open (Config::is_enabled) -==> 2. Abilities registered - ok: 7 abilities registered under wp-dev-tools/ -==> 3. Dedicated MCP route - ok: /wp-json/wp-dev-tools/mcp responds (HTTP 401) -==> 4. STDIO tools/list - ok: negotiated MCP protocol version 2025-06-18 - ok: tools/list is exactly the 7-tool allow-list -==> 5. Capture loop - ok: list-requests returned both tagged captures - ok: get-telemetry returned the normalised sections - ok: host file:line translation is working - ok: compare-requests returned deltas -==> 6. CWV beacon - ok: the beacon script and its config are enqueued on the front end -==> 7. Default adapter server isolation - ok: the default server exposes no wp-dev-tools-* tools - ok: the default server is up (discover-abilities present), so the check is meaningful - -==> All checks passed. -``` - -Exit code `0`. - -## Troubleshooting - -| Symptom | Cause | Fix | -|---|---|---| -| `RT_DEV_TOOLS_BOOTED is not defined` | The plugin is inactive, or the package is not installed | `wp plugin activate "$(basename "$PWD")"`; `composer update rtcamp/wp-dev-tools -W` | -| Booted, but `the dev gate is closed` | `RT_DEV_TOOLS_DEV_MODE` or `WP_ENVIRONMENT_TYPE` missing from the environment | Check `.wp-env.override.json`, then `npm run wp-env start` — constants only reach `wp-config.php` on a restart | -| Every ability errors | Same as above: the gate is shut | As above | -| Route returns `404` | The MCP Adapter plugin is not active | Confirm it is in `.wp-env.override.json`'s `plugins`, then restart | -| Route returns `000` | The site hung or dropped the connection | `npm run wp-env start`; check `npx wp-env logs` for a fatal | -| `tools/list` is empty, or the session never starts | `--user=admin` missing | Every ability is `manage_options`-gated and fails anonymously; the `dev:connect` script already passes `--user=admin` | -| Every `host_file` is `null` | Wrong `CONTAINER_ROOT`/`HOST_ROOT` pair | Re-run `npm run init -- --enable=dev-tools`; it derives both from the current checkout | -| `list-requests` is missing the tagged captures | Nothing was captured, or Query Monitor is inactive | `curl` the site once and retry; confirm Query Monitor is active | - -## Related - -- [dev-tools-demo.md](../dev-tools-demo.md) — the seven-beat guided demo. -- [maintenance.md](maintenance.md#release-validation) — the full release validation this check belongs to. diff --git a/docs/internal/maintenance.md b/docs/internal/maintenance.md index 96b79d3..63ec94b 100644 --- a/docs/internal/maintenance.md +++ b/docs/internal/maintenance.md @@ -26,7 +26,7 @@ The skeleton is a consumer of three shared repositories. Fix a problem where it - **`source`**: the placeholder identity (`Project Name`, `Project_Name\Features`, `rtcamp/plugin-elementary`) the engine search-replaces. The engine never rewrites files under `bin/`, so placeholders there are safe. - **`versionFiles`**: where the version is written (main-file header, `package.json`). - **`examples.groups`**: one entry per example set, built with `capability( key, label, category, { module, strip, remove, tests } )`, plus one `workflow()` entry for the CI caller. -- **`features`**: the toggleable features (`tailwind`, `hmr`, and `dev-tools` from `bin/features/dev-tools.js`). +- **`features`**: the toggleable features (`tailwind` and `hmr`). - **`cleanup.targets`**: paths deleted after setup (currently only `tests/js/scaffold-config.test.js`). ### Markers @@ -68,7 +68,7 @@ Record `git rev-parse HEAD`, Node and PHP versions, the framework revision in `c 1. Follow [Getting Started](../getting-started.md) in a fresh clone with a fresh dependency install from the declared sources. Do not substitute a local engine. 2. Run interactive init and a separate non-interactive setup. Check identity, kept sets, `.wp-scaffold.json`, and the Git decision. 3. Exercise each example-set removal in a fresh clone. Check deleted files and the remaining registration, then run `composer dump-autoload` and `php -l` on touched files before loading WordPress. The engine's own output (renamed, removed, toggled) is the primary signal; do not diff `inc/` by hand. -4. Enable and disable each optional feature. Install the changed declarations before checking Tailwind output; run the [Dev Tools check](dev-tools-e2e.md) for `dev-tools`. +4. Enable and disable each optional feature. Install the changed declarations before checking Tailwind output. 5. Start WordPress, activate the plugin, build, and confirm a source edit on the frontend and in the editor. 6. Follow the [scaffolding example](../scaffolding.md) and the [manual examples](../../DEVELOPMENT.md). Verify paths, registration, behaviour and focused tests. 7. Run the [checks](#checks) and build the documentation (see [Documentation publishing](#documentation-publishing)). diff --git a/tests/js/scaffold-config.test.js b/tests/js/scaffold-config.test.js deleted file mode 100644 index 475987d..0000000 --- a/tests/js/scaffold-config.test.js +++ /dev/null @@ -1,358 +0,0 @@ -/** - * dev-tools feature hooks from bin/scaffold.config.js. - * - * Driven through the real FeatureApi rather than a stand-in, so the hooks are - * exercised against the same editJson/read/write semantics the init engine - * uses. The declarative `apply` half (scripts) is the engine's job and is - * covered by its own tests; this file covers the decisions the config makes. - */ - -const fs = require( 'fs' ); -const os = require( 'os' ); -const path = require( 'path' ); - -const { makeFeatureApi } = require( '@rtcamp/wp-tooling/init' ); -const config = require( '../../bin/scaffold.config.js' ); - -const OVERRIDE = '.wp-env.override.json'; -const WP_ENV = '.wp-env.json'; -const QM = 'https://downloads.wordpress.org/plugin/query-monitor.zip'; -const ADAPTER = - 'https://github.com/WordPress/mcp-adapter/releases/download/v0.5.0/mcp-adapter.zip'; - -const feature = config.features.find( ( f ) => 'dev-tools' === f.key ); - -const write = ( root, rel, body ) => - fs.writeFileSync( path.join( root, rel ), body ); - -const readJson = ( root, rel ) => - JSON.parse( fs.readFileSync( path.join( root, rel ), 'utf8' ) ); - -const exists = ( root, rel ) => fs.existsSync( path.join( root, rel ) ); - -/** - * Project fixture with a committed wp-env config listing `plugins`. - * - * @param {Array} basePlugins - env.development.plugins in .wp-env.json. - * @param {string} kind - Consumer kind ('plugin' or 'theme'). - * @return {Object} { root, api }. - */ -const setup = ( basePlugins, kind = 'plugin' ) => { - const root = fs.mkdtempSync( path.join( os.tmpdir(), 'dev-tools-' ) ); - const base = basePlugins - ? { env: { development: { plugins: basePlugins } } } - : { core: null }; - write( root, WP_ENV, `${ JSON.stringify( base, null, '\t' ) }\n` ); - write( root, 'composer.json', '{\n\t"name": "acme/demo"\n}\n' ); - write( root, 'package.json', '{\n\t"name": "demo"\n}\n' ); - write( root, '.gitignore', `/${ OVERRIDE }\n` ); - - const api = makeFeatureApi( - root, - { kind, slug: 'demo' }, - { info: () => {}, warn: () => {} } - ); - return { root, api }; -}; - -/** - * Rewrite the committed plugin list, simulating a teammate adding one. - * - * @param {string} root - Project root. - * @param {Array} plugins - New env.development.plugins. - * @return {void} - */ -const setBasePlugins = ( root, plugins ) => { - const json = readJson( root, WP_ENV ); - json.env.development.plugins = plugins; - write( root, WP_ENV, `${ JSON.stringify( json, null, '\t' ) }\n` ); -}; - -describe( 'dev-tools feature', () => { - let root; - let api; - - afterEach( () => { - if ( root ) { - fs.rmSync( root, { recursive: true, force: true } ); - root = null; - } - } ); - - describe( 'enable', () => { - beforeEach( () => { - ( { root, api } = setup( [ '.' ] ) ); - } ); - - it( 'adds the composer dependency and its VCS repository', () => { - feature.onEnable( api ); - - const composer = readJson( root, 'composer.json' ); - expect( composer[ 'require-dev' ] ).toHaveProperty( - 'rtcamp/wp-dev-tools' - ); - expect( composer.repositories ).toEqual( [ - expect.objectContaining( { - type: 'vcs', - url: 'https://github.com/rtCamp/wp-devtools.git', - } ), - ] ); - } ); - - it( 'leaves the committed .wp-env.json untouched', () => { - const before = fs.readFileSync( path.join( root, WP_ENV ), 'utf8' ); - - feature.onEnable( api ); - - expect( fs.readFileSync( path.join( root, WP_ENV ), 'utf8' ) ).toBe( - before - ); - } ); - - it( 'writes the override with the base list plus the dev plugins', () => { - feature.onEnable( api ); - - const development = readJson( root, OVERRIDE ).env.development; - expect( development.plugins ).toEqual( [ '.', QM, ADAPTER ] ); - expect( development.config ).toEqual( { - WP_ENVIRONMENT_TYPE: 'local', - SAVEQUERIES: true, - RT_DEV_TOOLS_DEV_MODE: true, - RT_DEV_TOOLS_TELEMETRY_CONTAINER_ROOT: `/var/www/html/wp-content/plugins/${ path.basename( - root - ) }`, - RT_DEV_TOOLS_TELEMETRY_HOST_ROOT: root, - RT_DEV_TOOLS_TELEMETRY_LOOPBACK_BASE: 'http://wordpress', - } ); - } ); - - it( 'never writes the tests environment', () => { - feature.onEnable( api ); - - expect( readJson( root, OVERRIDE ).env.tests ).toBeUndefined(); - } ); - - it( 'reads a top-level plugins list from the committed config', () => { - write( - root, - WP_ENV, - `${ JSON.stringify( - { - core: null, - plugins: [ '.', 'team-plugin.zip' ], - testsEnvironment: false, - }, - null, - '\t' - ) }\n` - ); - - feature.onEnable( api ); - - expect( - readJson( root, OVERRIDE ).env.development.plugins - ).toEqual( [ '.', 'team-plugin.zip', QM, ADAPTER ] ); - } ); - - it( 'is idempotent -- no duplicate repository or plugin entries', () => { - feature.onEnable( api ); - const composer = fs.readFileSync( - path.join( root, 'composer.json' ), - 'utf8' - ); - - feature.onEnable( api ); - - expect( - fs.readFileSync( path.join( root, 'composer.json' ), 'utf8' ) - ).toBe( composer ); - expect( - readJson( root, OVERRIDE ).env.development.plugins - ).toHaveLength( 3 ); - } ); - - it( 'defers next steps rather than printing mid-spinner', () => { - feature.onEnable( api ); - - expect( api._notes.length ).toBeGreaterThan( 0 ); - expect( api._notes[ 0 ] ).toContain( 'composer update' ); - } ); - - it( 'refreshes from the committed list on re-enable', () => { - feature.onEnable( api ); - setBasePlugins( root, [ '.', 'team-plugin.zip' ] ); - - feature.onEnable( api ); - - expect( - readJson( root, OVERRIDE ).env.development.plugins - ).toEqual( [ '.', 'team-plugin.zip', QM, ADAPTER ] ); - } ); - } ); - - describe( 'disable', () => { - beforeEach( () => { - ( { root, api } = setup( [ '.' ] ) ); - } ); - - it( 'leaves no trace of itself', () => { - feature.onEnable( api ); - - feature.onDisable( api ); - - const composer = readJson( root, 'composer.json' ); - expect( composer[ 'require-dev' ] ).toBeUndefined(); - expect( composer.repositories ).toBeUndefined(); - expect( exists( root, OVERRIDE ) ).toBe( false ); - expect( feature.detect( api ) ).toBe( false ); - } ); - - // The empty-container case above only happens in a bare fixture; a real - // project has its own dev deps and VCS entries, which must survive. - it( "keeps the project's other dev deps and repositories", () => { - api.editJson( 'composer.json', ( json ) => { - json[ 'require-dev' ] = { 'phpunit/phpunit': '^9.6' }; - json.repositories = [ - { - type: 'vcs', - url: 'https://github.com/rtCamp/wp-primitives.git', - }, - ]; - } ); - feature.onEnable( api ); - - feature.onDisable( api ); - - const composer = readJson( root, 'composer.json' ); - expect( composer[ 'require-dev' ] ).toEqual( { - 'phpunit/phpunit': '^9.6', - } ); - expect( composer.repositories ).toEqual( [ - { - type: 'vcs', - url: 'https://github.com/rtCamp/wp-primitives.git', - }, - ] ); - } ); - - it( 'says how to drop the package from the lock and vendor', () => { - feature.onEnable( api ); - api._notes.length = 0; - - feature.onDisable( api ); - - expect( api._notes.join( '\n' ) ).toContain( 'composer update' ); - } ); - - // Regression: an exact base/remainder comparison kept `plugins: ["."]` - // pinned in the override once .wp-env.json had moved on. wp-env replaces - // that array, so the dev env silently lost the newly added plugin. - it( 'drops the plugin pin when the committed list has moved on', () => { - feature.onEnable( api ); - setBasePlugins( root, [ '.', 'team-plugin.zip' ] ); - - feature.onDisable( api ); - - expect( exists( root, OVERRIDE ) ).toBe( false ); - } ); - - it( "keeps a developer's own override plugin, refreshed against the base", () => { - feature.onEnable( api ); - api.editJson( OVERRIDE, ( json ) => { - json.env.development.plugins.push( 'my-own.zip' ); - } ); - setBasePlugins( root, [ '.', 'team-plugin.zip' ] ); - - feature.onDisable( api ); - - expect( readJson( root, OVERRIDE ).env.development ).toEqual( { - plugins: [ '.', 'team-plugin.zip', 'my-own.zip' ], - } ); - } ); - - it( "keeps a developer's own override config key", () => { - feature.onEnable( api ); - api.editJson( OVERRIDE, ( json ) => { - json.env.development.config.MY_OWN_KEY = 'keep me'; - } ); - - feature.onDisable( api ); - - expect( readJson( root, OVERRIDE ).env.development.config ).toEqual( - { MY_OWN_KEY: 'keep me' } - ); - } ); - - it( 'is a no-op when the override was never written', () => { - expect( () => feature.onDisable( api ) ).not.toThrow(); - expect( exists( root, OVERRIDE ) ).toBe( false ); - } ); - } ); - - describe( 'themes', () => { - it( 'omits "." and targets the themes bucket', () => { - ( { root, api } = setup( null, 'theme' ) ); - - feature.onEnable( api ); - - const development = readJson( root, OVERRIDE ).env.development; - expect( development.plugins ).toEqual( [ QM, ADAPTER ] ); - expect( - development.config.RT_DEV_TOOLS_TELEMETRY_CONTAINER_ROOT - ).toBe( - `/var/www/html/wp-content/themes/${ path.basename( root ) }` - ); - } ); - } ); - - describe( 'detect', () => { - beforeEach( () => { - ( { root, api } = setup( [ '.' ] ) ); - } ); - - it( 'is true only when both halves are present', () => { - expect( feature.detect( api ) ).toBe( false ); - - feature.onEnable( api ); - - expect( feature.detect( api ) ).toBe( true ); - } ); - - // A teammate's fresh clone has the committed dependency but not the - // gitignored override, and nothing actually works there. - it( 'is false with the composer entry alone', () => { - feature.onEnable( api ); - fs.rmSync( path.join( root, OVERRIDE ) ); - - expect( feature.detect( api ) ).toBe( false ); - } ); - - it( 'is false with the override alone', () => { - feature.onEnable( api ); - api.editJson( 'composer.json', ( json ) => { - delete json[ 'require-dev' ][ 'rtcamp/wp-dev-tools' ]; - } ); - - expect( feature.detect( api ) ).toBe( false ); - } ); - - // detect also runs read-only under `init --list`, before any rename. - it( 'is total against an empty project and malformed JSON', () => { - const bare = fs.mkdtempSync( path.join( os.tmpdir(), 'bare-' ) ); - try { - const bareApi = makeFeatureApi( - bare, - { kind: 'plugin', slug: 'demo' }, - {} - ); - expect( feature.detect( bareApi ) ).toBe( false ); - - write( bare, 'composer.json', '{ not json' ); - expect( () => feature.detect( bareApi ) ).not.toThrow(); - expect( feature.detect( bareApi ) ).toBe( false ); - } finally { - fs.rmSync( bare, { recursive: true, force: true } ); - } - } ); - } ); -} ); From 39ad8eee1108e6fe6ebcc8f1afcad8adc717e15a Mon Sep 17 00:00:00 2001 From: Aryan Jasala Date: Fri, 2 Oct 2026 04:28:28 +0530 Subject: [PATCH 2/2] chore(init): drop the cleanup step Its only target was tests/js/scaffold-config.test.js, removed with the Dev Tools feature. --- bin/scaffold.config.js | 4 +--- docs/initialization.md | 4 ++-- docs/internal/maintenance.md | 2 -- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/bin/scaffold.config.js b/bin/scaffold.config.js index bcff7dc..846bb3f 100644 --- a/bin/scaffold.config.js +++ b/bin/scaffold.config.js @@ -102,7 +102,7 @@ module.exports = { { path: 'package.json', kind: 'json', key: 'version' }, ], - steps: { composer: true, cleanup: true, git: true, hooks: true }, + steps: { composer: true, cleanup: false, git: true, hooks: true }, // Dep/build-config-carrying capabilities. Toggling adds/removes their deps. featuresDir: 'bin/features', @@ -228,8 +228,6 @@ module.exports = { ], }, - cleanup: { targets: [ 'tests/js/scaffold-config.test.js' ] }, - docsUrl: 'https://github.com/rtCamp/plugin-elementary/blob/main/docs/index.md', repoUrl: 'https://github.com/rtCamp/plugin-elementary', diff --git a/docs/initialization.md b/docs/initialization.md index 9f45f70..c9a29b0 100644 --- a/docs/initialization.md +++ b/docs/initialization.md @@ -39,7 +39,7 @@ Init rewrites and deletes files in place. A clean checkout is your only undo. `.wp-scaffold.json` stores these values. The generated code adds `-features`: plugin header `Acme Content Features`, text domain `acme-content-features`, prefixes `acme_content_features_` and `ACME_CONTENT_FEATURES_`, and main file `acme-content-features.php`. 3. **Select capabilities.** One grouped prompt lists the twelve example sets, the CI workflow and the three optional features. Space toggles, Enter confirms. Unchecking an example set removes it entirely. -4. **Apply.** Init rewrites the tokens, removes deselected sets, writes `.wp-scaffold.json`, regenerates the Composer autoloader, and runs cleanup. The wrapper then runs `npm run sync-ai`. +4. **Apply.** Init rewrites the tokens, removes deselected sets, writes `.wp-scaffold.json`, and regenerates the Composer autoloader. The wrapper then runs `npm run sync-ai`. 5. **Git and hooks.** A first run can start a new Git repository. This defaults to **No**; accepting deletes the existing `.git` and the skeleton's history, so decline it when keeping that history or working inside another repository. Non-interactive `--yes` skips optional Git setup. Defaults: every example set kept, HMR on, Tailwind and Dev Tools off. @@ -74,7 +74,7 @@ What each set contains is listed in [Included features](features.md). | Kept example sets | Stay as working references. Their `// wp:example:` markers are removed. | | Optional features | `hmr` edits `.env.local`; `tailwind` adds `src/css/tailwind.css`, `postcss.config.js`, dependencies and flips the Tailwind constant. | | State | `.wp-scaffold.json` records identity and feature choices. Do not edit it by hand. | -| Cleanup | Removes `tests/js/scaffold-config.test.js`, which only tests the skeleton's own init config. Copilot files, workflows and templates under `.github/` stay. | +| Cleanup | Nothing is removed. Copilot files, workflows and templates under `.github/` stay. | | AI instructions | `sync-ai` refreshes `.github/instructions/framework-php.instructions.md` from the installed framework. | Init does not generate a translation template. Run `npm run pot` when preparing translations. diff --git a/docs/internal/maintenance.md b/docs/internal/maintenance.md index 63ec94b..75d1505 100644 --- a/docs/internal/maintenance.md +++ b/docs/internal/maintenance.md @@ -27,7 +27,6 @@ The skeleton is a consumer of three shared repositories. Fix a problem where it - **`versionFiles`**: where the version is written (main-file header, `package.json`). - **`examples.groups`**: one entry per example set, built with `capability( key, label, category, { module, strip, remove, tests } )`, plus one `workflow()` entry for the CI caller. - **`features`**: the toggleable features (`tailwind` and `hmr`). -- **`cleanup.targets`**: paths deleted after setup (currently only `tests/js/scaffold-config.test.js`). ### Markers @@ -46,7 +45,6 @@ Rules that keep init working for every downstream project: - Keep marker pairs balanced and on their own lines. A broken pair breaks removal for everyone. - A capability's footprint must be complete: module file, class folder, test file, and every coupled region. Anything left behind references deleted classes. - Markers are consumed by the first run, so removal is one-shot by design. -- [`tests/js/scaffold-config.test.js`](../../tests/js/scaffold-config.test.js) asserts the config's shape; run `npm run test:js` after editing it. ### Add an example set