@ai-kits/wp-ag-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ANTIGRAVITY-README.md +47 -0
- package/CONTRIBUTING.md +122 -0
- package/README.md +135 -0
- package/STRUCTURE.md +200 -0
- package/agents/wordpress-expert.md +36 -0
- package/agents/wp-frontend-expert.md +21 -0
- package/bin/antigravity-agent.js +159 -0
- package/docs/authoring-guide.md +56 -0
- package/docs/compatibility-policy.md +18 -0
- package/docs/packaging.md +26 -0
- package/docs/principles.md +7 -0
- package/docs/skill-set-v1.md +21 -0
- package/docs/upstream-sync.md +52 -0
- package/package.json +47 -0
- package/rules/GEMINI.md +273 -0
- package/shared/references/.gitkeep +1 -0
- package/shared/references/gutenberg-releases.json +155 -0
- package/shared/references/wordpress-core-versions.json +208 -0
- package/shared/references/wp-gutenberg-version-map.json +886 -0
- package/shared/scripts/ai-generate-updates.mjs +458 -0
- package/shared/scripts/scaffold-skill.mjs +62 -0
- package/shared/scripts/skillpack-build.mjs +165 -0
- package/shared/scripts/skillpack-install.mjs +275 -0
- package/shared/scripts/update-upstream-indices.mjs +173 -0
- package/skills/wordpress-router/SKILL.md +51 -0
- package/skills/wordpress-router/references/decision-tree.md +55 -0
- package/skills/wp-abilities-api/SKILL.md +95 -0
- package/skills/wp-abilities-api/references/php-registration.md +67 -0
- package/skills/wp-abilities-api/references/rest-api.md +13 -0
- package/skills/wp-block-development/SKILL.md +174 -0
- package/skills/wp-block-development/references/attributes-and-serialization.md +22 -0
- package/skills/wp-block-development/references/block-json.md +49 -0
- package/skills/wp-block-development/references/creating-new-blocks.md +46 -0
- package/skills/wp-block-development/references/debugging.md +36 -0
- package/skills/wp-block-development/references/deprecations.md +24 -0
- package/skills/wp-block-development/references/dynamic-rendering.md +23 -0
- package/skills/wp-block-development/references/inner-blocks.md +25 -0
- package/skills/wp-block-development/references/registration.md +30 -0
- package/skills/wp-block-development/references/supports-and-wrappers.md +18 -0
- package/skills/wp-block-development/references/tooling-and-testing.md +21 -0
- package/skills/wp-block-development/scripts/list_blocks.mjs +121 -0
- package/skills/wp-block-themes/SKILL.md +116 -0
- package/skills/wp-block-themes/references/creating-new-block-theme.md +37 -0
- package/skills/wp-block-themes/references/debugging.md +24 -0
- package/skills/wp-block-themes/references/patterns.md +18 -0
- package/skills/wp-block-themes/references/style-variations.md +14 -0
- package/skills/wp-block-themes/references/templates-and-parts.md +16 -0
- package/skills/wp-block-themes/references/theme-json.md +59 -0
- package/skills/wp-block-themes/scripts/detect_block_themes.mjs +117 -0
- package/skills/wp-interactivity-api/SKILL.md +179 -0
- package/skills/wp-interactivity-api/references/debugging.md +29 -0
- package/skills/wp-interactivity-api/references/directives-quickref.md +30 -0
- package/skills/wp-interactivity-api/references/server-side-rendering.md +310 -0
- package/skills/wp-performance/SKILL.md +146 -0
- package/skills/wp-performance/references/autoload-options.md +24 -0
- package/skills/wp-performance/references/cron.md +20 -0
- package/skills/wp-performance/references/database.md +20 -0
- package/skills/wp-performance/references/http-api.md +15 -0
- package/skills/wp-performance/references/measurement.md +21 -0
- package/skills/wp-performance/references/object-cache.md +24 -0
- package/skills/wp-performance/references/query-monitor-headless.md +38 -0
- package/skills/wp-performance/references/server-timing.md +22 -0
- package/skills/wp-performance/references/wp-cli-doctor.md +24 -0
- package/skills/wp-performance/references/wp-cli-profile.md +32 -0
- package/skills/wp-performance/scripts/perf_inspect.mjs +128 -0
- package/skills/wp-phpstan/SKILL.md +97 -0
- package/skills/wp-phpstan/references/configuration.md +52 -0
- package/skills/wp-phpstan/references/third-party-classes.md +76 -0
- package/skills/wp-phpstan/references/wordpress-annotations.md +124 -0
- package/skills/wp-phpstan/scripts/phpstan_inspect.mjs +263 -0
- package/skills/wp-playground/SKILL.md +101 -0
- package/skills/wp-playground/references/blueprints.md +36 -0
- package/skills/wp-playground/references/cli-commands.md +39 -0
- package/skills/wp-playground/references/debugging.md +16 -0
- package/skills/wp-plugin-development/SKILL.md +112 -0
- package/skills/wp-plugin-development/references/data-and-cron.md +19 -0
- package/skills/wp-plugin-development/references/debugging.md +19 -0
- package/skills/wp-plugin-development/references/lifecycle.md +33 -0
- package/skills/wp-plugin-development/references/security.md +29 -0
- package/skills/wp-plugin-development/references/settings-api.md +22 -0
- package/skills/wp-plugin-development/references/structure.md +16 -0
- package/skills/wp-plugin-development/scripts/detect_plugins.mjs +122 -0
- package/skills/wp-project-triage/SKILL.md +38 -0
- package/skills/wp-project-triage/references/triage.schema.json +143 -0
- package/skills/wp-project-triage/scripts/detect_wp_project.mjs +592 -0
- package/skills/wp-rest-api/SKILL.md +114 -0
- package/skills/wp-rest-api/references/authentication.md +18 -0
- package/skills/wp-rest-api/references/custom-content-types.md +20 -0
- package/skills/wp-rest-api/references/discovery-and-params.md +20 -0
- package/skills/wp-rest-api/references/responses-and-fields.md +30 -0
- package/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
- package/skills/wp-rest-api/references/schema.md +22 -0
- package/skills/wp-wpcli-and-ops/SKILL.md +123 -0
- package/skills/wp-wpcli-and-ops/references/automation.md +30 -0
- package/skills/wp-wpcli-and-ops/references/cron-and-cache.md +23 -0
- package/skills/wp-wpcli-and-ops/references/debugging.md +17 -0
- package/skills/wp-wpcli-and-ops/references/multisite.md +22 -0
- package/skills/wp-wpcli-and-ops/references/packages-and-updates.md +22 -0
- package/skills/wp-wpcli-and-ops/references/safety.md +30 -0
- package/skills/wp-wpcli-and-ops/references/search-replace.md +40 -0
- package/skills/wp-wpcli-and-ops/scripts/wpcli_inspect.mjs +90 -0
- package/skills/wpds/SKILL.md +58 -0
- package/workflows/create-block.md +27 -0
- package/workflows/wp-lint.md +27 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
function usage() {
|
|
6
|
+
process.stderr.write(
|
|
7
|
+
[
|
|
8
|
+
"Usage:",
|
|
9
|
+
" node shared/scripts/skillpack-install.mjs --dest=<repo-root> [options]",
|
|
10
|
+
"",
|
|
11
|
+
"Options:",
|
|
12
|
+
" --dest=<path> Destination repo root (required, unless using --global)",
|
|
13
|
+
" --from=<path> Source directory (default: dist)",
|
|
14
|
+
" --targets=<list> Comma-separated targets: codex, vscode, claude, claude-global (default: codex,vscode)",
|
|
15
|
+
" --skills=<list> Comma-separated skill names to install (default: all)",
|
|
16
|
+
" --mode=<mode> 'replace' (default) or 'merge'",
|
|
17
|
+
" --global Shorthand for --targets=claude-global (installs to ~/.claude/skills)",
|
|
18
|
+
" --dry-run Show what would be installed without making changes",
|
|
19
|
+
" --list List available skills and exit",
|
|
20
|
+
"",
|
|
21
|
+
"Targets:",
|
|
22
|
+
" codex Install to <dest>/.codex/skills/",
|
|
23
|
+
" vscode Install to <dest>/.github/skills/",
|
|
24
|
+
" claude Install to <dest>/.claude/skills/ (project-level)",
|
|
25
|
+
" claude-global Install to ~/.claude/skills/ (user-level, ignores --dest)",
|
|
26
|
+
"",
|
|
27
|
+
"Examples:",
|
|
28
|
+
" # Build and install to a WordPress project",
|
|
29
|
+
" node shared/scripts/skillpack-build.mjs --clean",
|
|
30
|
+
" node shared/scripts/skillpack-install.mjs --dest=../my-wp-repo --targets=codex,vscode,claude",
|
|
31
|
+
"",
|
|
32
|
+
" # Install globally for Claude Code (all skills)",
|
|
33
|
+
" node shared/scripts/skillpack-install.mjs --global",
|
|
34
|
+
"",
|
|
35
|
+
" # Install specific skills globally",
|
|
36
|
+
" node shared/scripts/skillpack-install.mjs --global --skills=wp-playground,wp-block-development",
|
|
37
|
+
"",
|
|
38
|
+
" # Install to project with specific skills",
|
|
39
|
+
" node shared/scripts/skillpack-install.mjs --dest=../my-repo --targets=claude --skills=wp-wpcli-and-ops",
|
|
40
|
+
"",
|
|
41
|
+
].join("\n")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseArgs(argv) {
|
|
46
|
+
const args = {
|
|
47
|
+
from: "dist",
|
|
48
|
+
dest: null,
|
|
49
|
+
targets: ["codex", "vscode"],
|
|
50
|
+
skills: [],
|
|
51
|
+
mode: "replace",
|
|
52
|
+
dryRun: false,
|
|
53
|
+
global: false,
|
|
54
|
+
list: false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
for (const a of argv) {
|
|
58
|
+
if (a === "--help" || a === "-h") args.help = true;
|
|
59
|
+
else if (a === "--dry-run") args.dryRun = true;
|
|
60
|
+
else if (a === "--global") args.global = true;
|
|
61
|
+
else if (a === "--list") args.list = true;
|
|
62
|
+
else if (a.startsWith("--from=")) args.from = a.slice("--from=".length);
|
|
63
|
+
else if (a.startsWith("--dest=")) args.dest = a.slice("--dest=".length);
|
|
64
|
+
else if (a.startsWith("--targets=")) args.targets = a.slice("--targets=".length).split(",").filter(Boolean);
|
|
65
|
+
else if (a.startsWith("--skills=")) args.skills = a.slice("--skills=".length).split(",").filter(Boolean);
|
|
66
|
+
else if (a.startsWith("--mode=")) args.mode = a.slice("--mode=".length);
|
|
67
|
+
else {
|
|
68
|
+
process.stderr.write(`Unknown arg: ${a}\n`);
|
|
69
|
+
args.help = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --global is shorthand for --targets=claude-global
|
|
74
|
+
if (args.global) {
|
|
75
|
+
args.targets = ["claude-global"];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return args;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assert(condition, message) {
|
|
82
|
+
if (!condition) throw new Error(message);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isSymlink(p) {
|
|
86
|
+
try {
|
|
87
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function copyFileSyncPreserveMode(src, dest) {
|
|
94
|
+
const st = fs.statSync(src);
|
|
95
|
+
fs.copyFileSync(src, dest);
|
|
96
|
+
fs.chmodSync(dest, st.mode);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function copyDir({ srcDir, destDir }) {
|
|
100
|
+
if (isSymlink(srcDir)) throw new Error(`Refusing to copy symlink dir: ${srcDir}`);
|
|
101
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
104
|
+
for (const ent of entries) {
|
|
105
|
+
if (ent.name === ".DS_Store") continue;
|
|
106
|
+
const src = path.join(srcDir, ent.name);
|
|
107
|
+
const dest = path.join(destDir, ent.name);
|
|
108
|
+
|
|
109
|
+
if (isSymlink(src)) throw new Error(`Refusing to copy symlink: ${src}`);
|
|
110
|
+
|
|
111
|
+
if (ent.isDirectory()) {
|
|
112
|
+
copyDir({ srcDir: src, destDir: dest });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (ent.isFile()) {
|
|
116
|
+
copyFileSyncPreserveMode(src, dest);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function listSkillDirs(skillsRoot) {
|
|
123
|
+
if (!fs.existsSync(skillsRoot)) return [];
|
|
124
|
+
return fs
|
|
125
|
+
.readdirSync(skillsRoot, { withFileTypes: true })
|
|
126
|
+
.filter((d) => d.isDirectory())
|
|
127
|
+
.map((d) => path.join(skillsRoot, d.name))
|
|
128
|
+
.filter((d) => fs.existsSync(path.join(d, "SKILL.md")));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const VALID_TARGETS = ["codex", "vscode", "claude", "claude-global"];
|
|
132
|
+
|
|
133
|
+
// Map target to source subdirectory in dist
|
|
134
|
+
function getSourceDir(fromDir, target) {
|
|
135
|
+
// claude-global uses the same source as claude
|
|
136
|
+
const sourceTarget = target === "claude-global" ? "claude" : target;
|
|
137
|
+
const targetDirMap = {
|
|
138
|
+
codex: path.join(fromDir, "codex", ".codex", "skills"),
|
|
139
|
+
vscode: path.join(fromDir, "vscode", ".github", "skills"),
|
|
140
|
+
claude: path.join(fromDir, "claude", ".claude", "skills"),
|
|
141
|
+
};
|
|
142
|
+
return targetDirMap[sourceTarget];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Map target to destination directory
|
|
146
|
+
function getDestDir(destRepoRoot, target) {
|
|
147
|
+
// claude-global doesn't need destRepoRoot
|
|
148
|
+
if (target === "claude-global") {
|
|
149
|
+
return path.join(os.homedir(), ".claude", "skills");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Other targets require destRepoRoot
|
|
153
|
+
const destDirMap = {
|
|
154
|
+
codex: path.join(destRepoRoot, ".codex", "skills"),
|
|
155
|
+
vscode: path.join(destRepoRoot, ".github", "skills"),
|
|
156
|
+
claude: path.join(destRepoRoot, ".claude", "skills"),
|
|
157
|
+
};
|
|
158
|
+
return destDirMap[target];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function installTarget({ fromDir, destRepoRoot, target, skillsFilter, mode, dryRun }) {
|
|
162
|
+
const srcSkillsRoot = getSourceDir(fromDir, target);
|
|
163
|
+
const destSkillsRoot = getDestDir(destRepoRoot, target);
|
|
164
|
+
|
|
165
|
+
assert(srcSkillsRoot, `Unknown target: ${target}`);
|
|
166
|
+
assert(fs.existsSync(srcSkillsRoot), `Missing source skillpack dir: ${srcSkillsRoot}. Did you run skillpack-build.mjs first?`);
|
|
167
|
+
|
|
168
|
+
let skillDirs = listSkillDirs(srcSkillsRoot);
|
|
169
|
+
assert(skillDirs.length > 0, `No skills found in: ${srcSkillsRoot}`);
|
|
170
|
+
|
|
171
|
+
// Filter skills if requested
|
|
172
|
+
if (skillsFilter.length > 0) {
|
|
173
|
+
const requested = new Set(skillsFilter);
|
|
174
|
+
const available = skillDirs.map((d) => path.basename(d));
|
|
175
|
+
|
|
176
|
+
// Validate requested skills exist
|
|
177
|
+
for (const s of requested) {
|
|
178
|
+
assert(available.includes(s), `Unknown skill: ${s}. Available: ${available.join(", ")}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
skillDirs = skillDirs.filter((d) => requested.has(path.basename(d)));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (dryRun) {
|
|
185
|
+
process.stdout.write(`[DRY-RUN] Would install ${skillDirs.length} skill(s) to ${destSkillsRoot}:\n`);
|
|
186
|
+
for (const d of skillDirs) {
|
|
187
|
+
process.stdout.write(` - ${path.basename(d)}\n`);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fs.mkdirSync(destSkillsRoot, { recursive: true });
|
|
193
|
+
|
|
194
|
+
for (const srcSkillDir of skillDirs) {
|
|
195
|
+
const name = path.basename(srcSkillDir);
|
|
196
|
+
const destSkillDir = path.join(destSkillsRoot, name);
|
|
197
|
+
|
|
198
|
+
if (mode === "replace") {
|
|
199
|
+
fs.rmSync(destSkillDir, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
copyDir({ srcDir: srcSkillDir, destDir: destSkillDir });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const location = target === "claude-global" ? destSkillsRoot : path.relative(destRepoRoot, destSkillsRoot) || ".";
|
|
206
|
+
process.stdout.write(`OK: installed ${skillDirs.length} skill(s) to ${location}\n`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function listAvailableSkills(fromDir) {
|
|
210
|
+
// Check all possible target sources
|
|
211
|
+
const sources = ["codex", "vscode", "claude"].map((t) => getSourceDir(fromDir, t)).filter((p) => fs.existsSync(p));
|
|
212
|
+
|
|
213
|
+
if (sources.length === 0) {
|
|
214
|
+
process.stderr.write("No built skills found. Run skillpack-build.mjs first.\n");
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const skillDirs = listSkillDirs(sources[0]);
|
|
219
|
+
process.stdout.write("Available skills:\n");
|
|
220
|
+
for (const d of skillDirs) {
|
|
221
|
+
process.stdout.write(` - ${path.basename(d)}\n`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function main() {
|
|
226
|
+
const args = parseArgs(process.argv.slice(2));
|
|
227
|
+
if (args.help) {
|
|
228
|
+
usage();
|
|
229
|
+
process.exit(2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const repoRoot = process.cwd();
|
|
233
|
+
const fromDir = path.isAbsolute(args.from) ? args.from : path.join(repoRoot, args.from);
|
|
234
|
+
|
|
235
|
+
// Handle --list
|
|
236
|
+
if (args.list) {
|
|
237
|
+
listAvailableSkills(fromDir);
|
|
238
|
+
process.exit(0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Validate targets
|
|
242
|
+
const targets = [...new Set(args.targets)];
|
|
243
|
+
for (const t of targets) {
|
|
244
|
+
assert(VALID_TARGETS.includes(t), `Invalid target: ${t}. Valid targets: ${VALID_TARGETS.join(", ")}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --dest is required unless only using claude-global
|
|
248
|
+
const needsDest = targets.some((t) => t !== "claude-global");
|
|
249
|
+
if (needsDest && !args.dest) {
|
|
250
|
+
process.stderr.write("Error: --dest is required for non-global targets.\n\n");
|
|
251
|
+
usage();
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const destRepoRoot = args.dest
|
|
256
|
+
? path.isAbsolute(args.dest)
|
|
257
|
+
? args.dest
|
|
258
|
+
: path.join(repoRoot, args.dest)
|
|
259
|
+
: null;
|
|
260
|
+
|
|
261
|
+
assert(args.mode === "replace" || args.mode === "merge", "mode must be 'replace' or 'merge'");
|
|
262
|
+
|
|
263
|
+
for (const target of targets) {
|
|
264
|
+
installTarget({
|
|
265
|
+
fromDir,
|
|
266
|
+
destRepoRoot,
|
|
267
|
+
target,
|
|
268
|
+
skillsFilter: args.skills,
|
|
269
|
+
mode: args.mode,
|
|
270
|
+
dryRun: args.dryRun,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
main();
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const SOURCES = {
|
|
5
|
+
wordpressCoreVersionCheck: "https://api.wordpress.org/core/version-check/1.7/",
|
|
6
|
+
gutenbergReleases: "https://api.github.com/repos/WordPress/gutenberg/releases?per_page=50",
|
|
7
|
+
wpGutenbergMapDoc:
|
|
8
|
+
"https://developer.wordpress.org/block-editor/contributors/versions-in-wordpress/",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function mkdirp(dirPath) {
|
|
12
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeJson(filePath, value) {
|
|
16
|
+
mkdirp(path.dirname(filePath));
|
|
17
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stripTags(html) {
|
|
21
|
+
return html
|
|
22
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
23
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
24
|
+
.replace(/<[^>]+>/g, " ")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function decodeHtml(text) {
|
|
30
|
+
return text
|
|
31
|
+
.replace(/ /g, " ")
|
|
32
|
+
.replace(/&/g, "&")
|
|
33
|
+
.replace(/</g, "<")
|
|
34
|
+
.replace(/>/g, ">")
|
|
35
|
+
.replace(/"/g, "\"")
|
|
36
|
+
.replace(/'/g, "'")
|
|
37
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchText(url) {
|
|
41
|
+
const res = await fetch(url, {
|
|
42
|
+
headers: {
|
|
43
|
+
"user-agent": "wp-agent-skills-upstream-sync/0.1",
|
|
44
|
+
accept: "text/html,application/json",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) throw new Error(`Fetch failed ${res.status} for ${url}`);
|
|
48
|
+
return await res.text();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchJson(url) {
|
|
52
|
+
const text = await fetchText(url);
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(text);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error(`Expected JSON from ${url}, got non-JSON response`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseWpGutenbergMapFromHtml(html) {
|
|
61
|
+
// Best-effort HTML table parsing without dependencies:
|
|
62
|
+
// 1) find the first <table> that contains "WordPress Version" and "Gutenberg Versions"
|
|
63
|
+
// 2) extract rows, then extract cells
|
|
64
|
+
const tables = [...html.matchAll(/<table[\s\S]*?<\/table>/gi)].map((m) => m[0]);
|
|
65
|
+
const target = tables.find((t) => /WordPress\s*Version/i.test(t) && /Gutenberg\s*Versions/i.test(t));
|
|
66
|
+
if (!target) return { rows: [], note: "table-not-found" };
|
|
67
|
+
|
|
68
|
+
const rowHtml = [...target.matchAll(/<tr[\s\S]*?<\/tr>/gi)].map((m) => m[0]);
|
|
69
|
+
const rows = [];
|
|
70
|
+
|
|
71
|
+
for (const r of rowHtml) {
|
|
72
|
+
const cellHtml = [...r.matchAll(/<(td|th)[^>]*>([\s\S]*?)<\/\1>/gi)].map((m) => m[2]);
|
|
73
|
+
if (cellHtml.length < 2) continue;
|
|
74
|
+
|
|
75
|
+
const cells = cellHtml.map((c) => decodeHtml(stripTags(c)));
|
|
76
|
+
const wp = cells[0];
|
|
77
|
+
const gb = cells[1];
|
|
78
|
+
|
|
79
|
+
if (/WordPress\s*Version/i.test(wp) || /Gutenberg\s*Versions/i.test(gb)) continue;
|
|
80
|
+
if (!/^\d+\.\d+/.test(wp)) continue;
|
|
81
|
+
|
|
82
|
+
rows.push({ wordpress: wp, gutenberg: gb });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { rows, note: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeWpVersionCheckPayload(payload) {
|
|
89
|
+
// https://api.wordpress.org/core/version-check/1.7/ returns something like:
|
|
90
|
+
// { offers: [...], translations: [...] }
|
|
91
|
+
const offers = Array.isArray(payload?.offers) ? payload.offers : [];
|
|
92
|
+
|
|
93
|
+
const candidates = offers
|
|
94
|
+
.map((o) => ({
|
|
95
|
+
version: typeof o?.version === "string" ? o.version : null,
|
|
96
|
+
current: typeof o?.current === "string" ? o.current : null,
|
|
97
|
+
download: typeof o?.download === "string" ? o.download : null,
|
|
98
|
+
phpVersion: typeof o?.php_version === "string" ? o.php_version : null,
|
|
99
|
+
mysqlVersion: typeof o?.mysql_version === "string" ? o.mysql_version : null,
|
|
100
|
+
response: typeof o?.response === "string" ? o.response : null,
|
|
101
|
+
locale: typeof o?.locale === "string" ? o.locale : null,
|
|
102
|
+
}))
|
|
103
|
+
.filter((o) => o.version || o.current);
|
|
104
|
+
|
|
105
|
+
// Keep a small, stable subset; prioritize "upgrade" offers.
|
|
106
|
+
const byVersion = new Map();
|
|
107
|
+
for (const o of candidates) {
|
|
108
|
+
const v = o.version ?? o.current;
|
|
109
|
+
if (!v) continue;
|
|
110
|
+
if (!byVersion.has(v)) byVersion.set(v, o);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const versions = [...byVersion.keys()].sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
|
|
114
|
+
return {
|
|
115
|
+
latest: versions[0] ?? null,
|
|
116
|
+
recent: versions.slice(0, 20),
|
|
117
|
+
offers: versions.slice(0, 20).map((v) => byVersion.get(v)),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeGutenbergReleases(payload) {
|
|
122
|
+
const releases = Array.isArray(payload) ? payload : [];
|
|
123
|
+
const stable = releases
|
|
124
|
+
.filter((r) => r && !r.draft && !r.prerelease && typeof r.tag_name === "string")
|
|
125
|
+
.map((r) => ({
|
|
126
|
+
tag: r.tag_name,
|
|
127
|
+
name: typeof r.name === "string" ? r.name : null,
|
|
128
|
+
publishedAt: typeof r.published_at === "string" ? r.published_at : null,
|
|
129
|
+
url: typeof r.html_url === "string" ? r.html_url : null,
|
|
130
|
+
}));
|
|
131
|
+
return {
|
|
132
|
+
latest: stable[0] ?? null,
|
|
133
|
+
recent: stable.slice(0, 30),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
const repoRoot = process.cwd();
|
|
139
|
+
const outDir = path.join(repoRoot, "shared", "references");
|
|
140
|
+
|
|
141
|
+
const [wpVersionPayload, gbReleasesPayload, mapHtml] = await Promise.all([
|
|
142
|
+
fetchJson(SOURCES.wordpressCoreVersionCheck),
|
|
143
|
+
fetchJson(SOURCES.gutenbergReleases),
|
|
144
|
+
fetchText(SOURCES.wpGutenbergMapDoc),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const wordpress = normalizeWpVersionCheckPayload(wpVersionPayload);
|
|
148
|
+
const gutenberg = normalizeGutenbergReleases(gbReleasesPayload);
|
|
149
|
+
const map = parseWpGutenbergMapFromHtml(mapHtml);
|
|
150
|
+
|
|
151
|
+
writeJson(path.join(outDir, "wordpress-core-versions.json"), {
|
|
152
|
+
source: SOURCES.wordpressCoreVersionCheck,
|
|
153
|
+
...wordpress,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
writeJson(path.join(outDir, "gutenberg-releases.json"), {
|
|
157
|
+
source: SOURCES.gutenbergReleases,
|
|
158
|
+
...gutenberg,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
writeJson(path.join(outDir, "wp-gutenberg-version-map.json"), {
|
|
162
|
+
source: SOURCES.wpGutenbergMapDoc,
|
|
163
|
+
note: map.note,
|
|
164
|
+
rows: map.rows,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
process.stdout.write("OK: updated shared/references/* upstream indices\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main().catch((err) => {
|
|
171
|
+
process.stderr.write(`${err?.stack || String(err)}\n`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wordpress-router
|
|
3
|
+
description: "Use when the user asks about WordPress codebases (plugins, themes, block themes, Gutenberg blocks, WP core checkouts) and you need to quickly classify the repo and route to the correct workflow/skill (blocks, theme.json, REST API, WP-CLI, performance, security, testing, release packaging)."
|
|
4
|
+
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# WordPress Router
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
Use this skill at the start of most WordPress tasks to:
|
|
12
|
+
|
|
13
|
+
- identify what kind of WordPress codebase this is (plugin vs theme vs block theme vs WP core checkout vs full site),
|
|
14
|
+
- pick the right workflow and guardrails,
|
|
15
|
+
- delegate to the most relevant domain skill(s).
|
|
16
|
+
|
|
17
|
+
## Inputs required
|
|
18
|
+
|
|
19
|
+
- Repo root (current working directory).
|
|
20
|
+
- The user’s intent (what they want changed) and any constraints (WP version targets, WP.com specifics, release requirements).
|
|
21
|
+
|
|
22
|
+
## Procedure
|
|
23
|
+
|
|
24
|
+
1. Run the project triage script:
|
|
25
|
+
- `node skills/wp-project-triage/scripts/detect_wp_project.mjs`
|
|
26
|
+
2. Read the triage output and classify:
|
|
27
|
+
- primary project kind(s),
|
|
28
|
+
- tooling available (PHP/Composer, Node, @wordpress/scripts),
|
|
29
|
+
- tests present (PHPUnit, Playwright, wp-env),
|
|
30
|
+
- any version hints.
|
|
31
|
+
3. Route to domain workflows based on user intent + repo kind:
|
|
32
|
+
- For the decision tree, read: `skills/wordpress-router/references/decision-tree.md`.
|
|
33
|
+
4. Apply guardrails before making changes:
|
|
34
|
+
- Confirm any version constraints if unclear.
|
|
35
|
+
- Prefer the repo’s existing tooling and conventions for builds/tests.
|
|
36
|
+
|
|
37
|
+
## Verification
|
|
38
|
+
|
|
39
|
+
- Re-run the triage script if you create or restructure significant files.
|
|
40
|
+
- Run the repo’s lint/test/build commands that the triage output recommends (if available).
|
|
41
|
+
|
|
42
|
+
## Failure modes / debugging
|
|
43
|
+
|
|
44
|
+
- If triage reports `kind: unknown`, inspect:
|
|
45
|
+
- root `composer.json`, `package.json`, `style.css`, `block.json`, `theme.json`, `wp-content/`.
|
|
46
|
+
- If the repo is huge, consider narrowing scanning scope or adding ignore rules to the triage script.
|
|
47
|
+
|
|
48
|
+
## Escalation
|
|
49
|
+
|
|
50
|
+
- If routing is ambiguous, ask one question:
|
|
51
|
+
- “Is this intended to be a WordPress plugin, a theme (classic/block), or a full site repo?”
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Router decision tree (v1)
|
|
2
|
+
|
|
3
|
+
This is a lightweight routing guide. It assumes you can run `wp-project-triage` first.
|
|
4
|
+
|
|
5
|
+
## Step 1: classify repo kind (from triage)
|
|
6
|
+
|
|
7
|
+
Use `triage.project.kind` and the strongest signals:
|
|
8
|
+
|
|
9
|
+
- `wp-core` → treat as WordPress core checkout work (core patches, PHPUnit, build tools).
|
|
10
|
+
- `wp-site` → treat as a full site repo (wp-content present; changes might be theme + plugins).
|
|
11
|
+
- `wp-block-theme` → theme.json/templates/patterns workflows.
|
|
12
|
+
- `wp-theme` → classic theme workflows (templates PHP, `functions.php`, `style.css`).
|
|
13
|
+
- `wp-block-plugin` → Gutenberg block development in a plugin (block.json, build pipeline).
|
|
14
|
+
- `wp-plugin` / `wp-mu-plugin` → plugin workflows (hooks, admin, settings, cron, REST, security).
|
|
15
|
+
- `gutenberg` → Gutenberg monorepo workflows (packages, tooling, docs).
|
|
16
|
+
|
|
17
|
+
If multiple kinds match, prefer the most specific:
|
|
18
|
+
`gutenberg` > `wp-core` > `wp-site` > `wp-block-theme` > `wp-block-plugin` > `wp-theme` > `wp-plugin`.
|
|
19
|
+
|
|
20
|
+
## Step 2: route by user intent (keywords)
|
|
21
|
+
|
|
22
|
+
Route by intent even if repo kind is broad (like `wp-site`):
|
|
23
|
+
|
|
24
|
+
- **Interactivity API / data-wp-* directives / @wordpress/interactivity / viewScriptModule**
|
|
25
|
+
- Route → `wp-interactivity-api`.
|
|
26
|
+
- **Abilities API / wp_register_ability / wp-abilities/v1 / @wordpress/abilities**
|
|
27
|
+
- Route → `wp-abilities-api`.
|
|
28
|
+
- **Playground / run-blueprint / build-snapshot / @wp-playground/cli / playground.wordpress.net**
|
|
29
|
+
- Route → `wp-playground`.
|
|
30
|
+
- **Blocks / block.json / registerBlockType / attributes / save serialization**
|
|
31
|
+
- Route → `wp-block-development`.
|
|
32
|
+
- **theme.json / Global Styles / templates/*.html / patterns/**
|
|
33
|
+
- Route → `wp-block-themes`.
|
|
34
|
+
- **Plugins / hooks / activation hook / uninstall / Settings API / admin pages**
|
|
35
|
+
- Route → `wp-plugin-development`.
|
|
36
|
+
- **REST endpoint / register_rest_route / permission_callback**
|
|
37
|
+
- Route → `wp-rest-api`.
|
|
38
|
+
- **WP-CLI / wp-cli.yml / commands**
|
|
39
|
+
- Route → `wp-wpcli-and-ops`.
|
|
40
|
+
- **Build tooling / @wordpress/scripts / webpack / Vite / npm scripts**
|
|
41
|
+
- Route → `wp-build-tooling` (planned).
|
|
42
|
+
- **Testing / PHPUnit / wp-env / Playwright**
|
|
43
|
+
- Route → `wp-testing` (planned).
|
|
44
|
+
- **PHPStan / static analysis / phpstan.neon / phpstan-baseline.neon**
|
|
45
|
+
- Route → `wp-phpstan`.
|
|
46
|
+
- **Performance / caching / query profiling / editor slowness**
|
|
47
|
+
- Route → `wp-performance`.
|
|
48
|
+
- **Security / nonces / capabilities / sanitization/escaping / uploads**
|
|
49
|
+
- Route → `wp-security` (planned).
|
|
50
|
+
|
|
51
|
+
## Step 3: guardrails checklist (always)
|
|
52
|
+
|
|
53
|
+
- Verify detected tooling before suggesting commands (Composer vs npm/yarn/pnpm).
|
|
54
|
+
- Prefer existing lint/test scripts if present.
|
|
55
|
+
- If version constraints aren’t detectable, ask for target WP core and PHP versions.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wp-abilities-api
|
|
3
|
+
description: "Use when working with the WordPress Abilities API (wp_register_ability, wp_register_ability_category, /wp-json/wp-abilities/v1/*, @wordpress/abilities) including defining abilities, categories, meta, REST exposure, and permissions checks for clients."
|
|
4
|
+
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# WP Abilities API
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
Use this skill when the task involves:
|
|
12
|
+
|
|
13
|
+
- registering abilities or ability categories in PHP,
|
|
14
|
+
- exposing abilities to clients via REST (`wp-abilities/v1`),
|
|
15
|
+
- consuming abilities in JS (notably `@wordpress/abilities`),
|
|
16
|
+
- diagnosing “ability doesn’t show up” / “client can’t see ability” / “REST returns empty”.
|
|
17
|
+
|
|
18
|
+
## Inputs required
|
|
19
|
+
|
|
20
|
+
- Repo root (run `wp-project-triage` first if you haven’t).
|
|
21
|
+
- Target WordPress version(s) and whether this is WP core or a plugin/theme.
|
|
22
|
+
- Where the change should live (plugin vs theme vs mu-plugin).
|
|
23
|
+
|
|
24
|
+
## Procedure
|
|
25
|
+
|
|
26
|
+
### 1) Confirm availability and version constraints
|
|
27
|
+
|
|
28
|
+
- If this is WP core work, check `signals.isWpCoreCheckout` and `versions.wordpress.core`.
|
|
29
|
+
- If the project targets WP < 6.9, you may need the Abilities API plugin/package rather than relying on core.
|
|
30
|
+
|
|
31
|
+
### 2) Find existing Abilities usage
|
|
32
|
+
|
|
33
|
+
Search for these in the repo:
|
|
34
|
+
|
|
35
|
+
- `wp_register_ability(`
|
|
36
|
+
- `wp_register_ability_category(`
|
|
37
|
+
- `wp_abilities_api_init`
|
|
38
|
+
- `wp_abilities_api_categories_init`
|
|
39
|
+
- `wp-abilities/v1`
|
|
40
|
+
- `@wordpress/abilities`
|
|
41
|
+
|
|
42
|
+
If none exist, decide whether you’re introducing Abilities API fresh (new registrations + client consumption) or only consuming.
|
|
43
|
+
|
|
44
|
+
### 3) Register categories (optional)
|
|
45
|
+
|
|
46
|
+
If you need a logical grouping, register an ability category early (see `references/php-registration.md`).
|
|
47
|
+
|
|
48
|
+
### 4) Register abilities (PHP)
|
|
49
|
+
|
|
50
|
+
Implement the ability in PHP registration with:
|
|
51
|
+
|
|
52
|
+
- stable `id` (namespaced),
|
|
53
|
+
- `label`/`description`,
|
|
54
|
+
- `category`,
|
|
55
|
+
- `meta`:
|
|
56
|
+
- add `readonly: true` when the ability is informational,
|
|
57
|
+
- set `show_in_rest: true` for abilities you want visible to clients.
|
|
58
|
+
|
|
59
|
+
Use the documented init hooks for Abilities API registration so they load at the right time (see `references/php-registration.md`).
|
|
60
|
+
|
|
61
|
+
### 5) Confirm REST exposure
|
|
62
|
+
|
|
63
|
+
- Verify the REST endpoints exist and return expected results (see `references/rest-api.md`).
|
|
64
|
+
- If the client still can’t see the ability, confirm `meta.show_in_rest` is enabled and you’re querying the right endpoint.
|
|
65
|
+
|
|
66
|
+
### 6) Consume from JS (if needed)
|
|
67
|
+
|
|
68
|
+
- Prefer `@wordpress/abilities` APIs for client-side access and checks.
|
|
69
|
+
- Ensure build tooling includes the dependency and the project’s build pipeline bundles it.
|
|
70
|
+
|
|
71
|
+
## Verification
|
|
72
|
+
|
|
73
|
+
- `wp-project-triage` indicates `signals.usesAbilitiesApi: true` after your change (if applicable).
|
|
74
|
+
- REST check (in a WP environment): endpoints under `wp-abilities/v1` return your ability and category when expected.
|
|
75
|
+
- If the repo has tests, add/update coverage near:
|
|
76
|
+
- PHP: ability registration and meta exposure
|
|
77
|
+
- JS: ability consumption and UI gating
|
|
78
|
+
|
|
79
|
+
## Failure modes / debugging
|
|
80
|
+
|
|
81
|
+
- Ability never appears:
|
|
82
|
+
- registration code not running (wrong hook / file not loaded),
|
|
83
|
+
- missing `meta.show_in_rest`,
|
|
84
|
+
- incorrect category/ID mismatch.
|
|
85
|
+
- REST shows ability but JS doesn’t:
|
|
86
|
+
- wrong REST base/namespace,
|
|
87
|
+
- JS dependency not bundled,
|
|
88
|
+
- caching (object/page caches) masking changes.
|
|
89
|
+
|
|
90
|
+
## Escalation
|
|
91
|
+
|
|
92
|
+
- If you’re uncertain about version support, confirm target WP core versions and whether Abilities API is expected from core or as a plugin.
|
|
93
|
+
- For canonical details, consult:
|
|
94
|
+
- `references/rest-api.md`
|
|
95
|
+
- `references/php-registration.md`
|