@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.
Files changed (104) hide show
  1. package/ANTIGRAVITY-README.md +47 -0
  2. package/CONTRIBUTING.md +122 -0
  3. package/README.md +135 -0
  4. package/STRUCTURE.md +200 -0
  5. package/agents/wordpress-expert.md +36 -0
  6. package/agents/wp-frontend-expert.md +21 -0
  7. package/bin/antigravity-agent.js +159 -0
  8. package/docs/authoring-guide.md +56 -0
  9. package/docs/compatibility-policy.md +18 -0
  10. package/docs/packaging.md +26 -0
  11. package/docs/principles.md +7 -0
  12. package/docs/skill-set-v1.md +21 -0
  13. package/docs/upstream-sync.md +52 -0
  14. package/package.json +47 -0
  15. package/rules/GEMINI.md +273 -0
  16. package/shared/references/.gitkeep +1 -0
  17. package/shared/references/gutenberg-releases.json +155 -0
  18. package/shared/references/wordpress-core-versions.json +208 -0
  19. package/shared/references/wp-gutenberg-version-map.json +886 -0
  20. package/shared/scripts/ai-generate-updates.mjs +458 -0
  21. package/shared/scripts/scaffold-skill.mjs +62 -0
  22. package/shared/scripts/skillpack-build.mjs +165 -0
  23. package/shared/scripts/skillpack-install.mjs +275 -0
  24. package/shared/scripts/update-upstream-indices.mjs +173 -0
  25. package/skills/wordpress-router/SKILL.md +51 -0
  26. package/skills/wordpress-router/references/decision-tree.md +55 -0
  27. package/skills/wp-abilities-api/SKILL.md +95 -0
  28. package/skills/wp-abilities-api/references/php-registration.md +67 -0
  29. package/skills/wp-abilities-api/references/rest-api.md +13 -0
  30. package/skills/wp-block-development/SKILL.md +174 -0
  31. package/skills/wp-block-development/references/attributes-and-serialization.md +22 -0
  32. package/skills/wp-block-development/references/block-json.md +49 -0
  33. package/skills/wp-block-development/references/creating-new-blocks.md +46 -0
  34. package/skills/wp-block-development/references/debugging.md +36 -0
  35. package/skills/wp-block-development/references/deprecations.md +24 -0
  36. package/skills/wp-block-development/references/dynamic-rendering.md +23 -0
  37. package/skills/wp-block-development/references/inner-blocks.md +25 -0
  38. package/skills/wp-block-development/references/registration.md +30 -0
  39. package/skills/wp-block-development/references/supports-and-wrappers.md +18 -0
  40. package/skills/wp-block-development/references/tooling-and-testing.md +21 -0
  41. package/skills/wp-block-development/scripts/list_blocks.mjs +121 -0
  42. package/skills/wp-block-themes/SKILL.md +116 -0
  43. package/skills/wp-block-themes/references/creating-new-block-theme.md +37 -0
  44. package/skills/wp-block-themes/references/debugging.md +24 -0
  45. package/skills/wp-block-themes/references/patterns.md +18 -0
  46. package/skills/wp-block-themes/references/style-variations.md +14 -0
  47. package/skills/wp-block-themes/references/templates-and-parts.md +16 -0
  48. package/skills/wp-block-themes/references/theme-json.md +59 -0
  49. package/skills/wp-block-themes/scripts/detect_block_themes.mjs +117 -0
  50. package/skills/wp-interactivity-api/SKILL.md +179 -0
  51. package/skills/wp-interactivity-api/references/debugging.md +29 -0
  52. package/skills/wp-interactivity-api/references/directives-quickref.md +30 -0
  53. package/skills/wp-interactivity-api/references/server-side-rendering.md +310 -0
  54. package/skills/wp-performance/SKILL.md +146 -0
  55. package/skills/wp-performance/references/autoload-options.md +24 -0
  56. package/skills/wp-performance/references/cron.md +20 -0
  57. package/skills/wp-performance/references/database.md +20 -0
  58. package/skills/wp-performance/references/http-api.md +15 -0
  59. package/skills/wp-performance/references/measurement.md +21 -0
  60. package/skills/wp-performance/references/object-cache.md +24 -0
  61. package/skills/wp-performance/references/query-monitor-headless.md +38 -0
  62. package/skills/wp-performance/references/server-timing.md +22 -0
  63. package/skills/wp-performance/references/wp-cli-doctor.md +24 -0
  64. package/skills/wp-performance/references/wp-cli-profile.md +32 -0
  65. package/skills/wp-performance/scripts/perf_inspect.mjs +128 -0
  66. package/skills/wp-phpstan/SKILL.md +97 -0
  67. package/skills/wp-phpstan/references/configuration.md +52 -0
  68. package/skills/wp-phpstan/references/third-party-classes.md +76 -0
  69. package/skills/wp-phpstan/references/wordpress-annotations.md +124 -0
  70. package/skills/wp-phpstan/scripts/phpstan_inspect.mjs +263 -0
  71. package/skills/wp-playground/SKILL.md +101 -0
  72. package/skills/wp-playground/references/blueprints.md +36 -0
  73. package/skills/wp-playground/references/cli-commands.md +39 -0
  74. package/skills/wp-playground/references/debugging.md +16 -0
  75. package/skills/wp-plugin-development/SKILL.md +112 -0
  76. package/skills/wp-plugin-development/references/data-and-cron.md +19 -0
  77. package/skills/wp-plugin-development/references/debugging.md +19 -0
  78. package/skills/wp-plugin-development/references/lifecycle.md +33 -0
  79. package/skills/wp-plugin-development/references/security.md +29 -0
  80. package/skills/wp-plugin-development/references/settings-api.md +22 -0
  81. package/skills/wp-plugin-development/references/structure.md +16 -0
  82. package/skills/wp-plugin-development/scripts/detect_plugins.mjs +122 -0
  83. package/skills/wp-project-triage/SKILL.md +38 -0
  84. package/skills/wp-project-triage/references/triage.schema.json +143 -0
  85. package/skills/wp-project-triage/scripts/detect_wp_project.mjs +592 -0
  86. package/skills/wp-rest-api/SKILL.md +114 -0
  87. package/skills/wp-rest-api/references/authentication.md +18 -0
  88. package/skills/wp-rest-api/references/custom-content-types.md +20 -0
  89. package/skills/wp-rest-api/references/discovery-and-params.md +20 -0
  90. package/skills/wp-rest-api/references/responses-and-fields.md +30 -0
  91. package/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
  92. package/skills/wp-rest-api/references/schema.md +22 -0
  93. package/skills/wp-wpcli-and-ops/SKILL.md +123 -0
  94. package/skills/wp-wpcli-and-ops/references/automation.md +30 -0
  95. package/skills/wp-wpcli-and-ops/references/cron-and-cache.md +23 -0
  96. package/skills/wp-wpcli-and-ops/references/debugging.md +17 -0
  97. package/skills/wp-wpcli-and-ops/references/multisite.md +22 -0
  98. package/skills/wp-wpcli-and-ops/references/packages-and-updates.md +22 -0
  99. package/skills/wp-wpcli-and-ops/references/safety.md +30 -0
  100. package/skills/wp-wpcli-and-ops/references/search-replace.md +40 -0
  101. package/skills/wp-wpcli-and-ops/scripts/wpcli_inspect.mjs +90 -0
  102. package/skills/wpds/SKILL.md +58 -0
  103. package/workflows/create-block.md +27 -0
  104. package/workflows/wp-lint.md +27 -0
@@ -0,0 +1,592 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+
5
+ const TOOL_VERSION = "0.1.0";
6
+
7
+ const DEFAULT_IGNORES = new Set([
8
+ ".git",
9
+ "node_modules",
10
+ "vendor",
11
+ "dist",
12
+ "build",
13
+ "coverage",
14
+ ".next",
15
+ ".turbo",
16
+ ]);
17
+
18
+ function statSafe(p) {
19
+ try {
20
+ return fs.statSync(p);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function readFileSafe(p, maxBytes = 256 * 1024) {
27
+ try {
28
+ const buf = fs.readFileSync(p);
29
+ if (buf.byteLength > maxBytes) return buf.subarray(0, maxBytes).toString("utf8");
30
+ return buf.toString("utf8");
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function scanForTokens(repoRoot, { tokens, exts, maxFiles = 2500, maxDepth = 8 }) {
37
+ const loweredTokens = tokens.map((t) => t.toLowerCase());
38
+ const matches = new Map();
39
+
40
+ const { results: files, truncated } = findFilesRecursive(
41
+ repoRoot,
42
+ (p) => {
43
+ const ext = path.extname(p).toLowerCase();
44
+ return exts.includes(ext);
45
+ },
46
+ { maxFiles, maxDepth }
47
+ );
48
+
49
+ for (const filePath of files) {
50
+ const contents = readFileSafe(filePath, 128 * 1024);
51
+ if (!contents) continue;
52
+ const haystack = contents.toLowerCase();
53
+
54
+ for (let i = 0; i < loweredTokens.length; i += 1) {
55
+ const token = loweredTokens[i];
56
+ if (matches.has(token)) continue;
57
+ if (haystack.includes(token)) matches.set(token, path.relative(repoRoot, filePath));
58
+ }
59
+ if (matches.size === loweredTokens.length) break;
60
+ }
61
+
62
+ return {
63
+ truncated,
64
+ matches: Object.fromEntries([...matches.entries()]),
65
+ };
66
+ }
67
+
68
+ function existsFile(p) {
69
+ const st = statSafe(p);
70
+ return Boolean(st && st.isFile());
71
+ }
72
+
73
+ function existsDir(p) {
74
+ const st = statSafe(p);
75
+ return Boolean(st && st.isDirectory());
76
+ }
77
+
78
+ function detectPackageManager(repoRoot) {
79
+ const hasPnpm = existsFile(path.join(repoRoot, "pnpm-lock.yaml"));
80
+ const hasYarn = existsFile(path.join(repoRoot, "yarn.lock"));
81
+ const hasNpm = existsFile(path.join(repoRoot, "package-lock.json"));
82
+ const hasBun = existsFile(path.join(repoRoot, "bun.lockb")) || existsFile(path.join(repoRoot, "bun.lock"));
83
+ if (hasPnpm) return "pnpm";
84
+ if (hasYarn) return "yarn";
85
+ if (hasBun) return "bun";
86
+ if (hasNpm) return "npm";
87
+ return null;
88
+ }
89
+
90
+ function findFilesRecursive(repoRoot, predicate, { maxFiles = 6000, maxDepth = 8 } = {}) {
91
+ const results = [];
92
+ const queue = [{ dir: repoRoot, depth: 0 }];
93
+ let visited = 0;
94
+
95
+ while (queue.length > 0) {
96
+ const { dir, depth } = queue.shift();
97
+ if (depth > maxDepth) continue;
98
+
99
+ let entries;
100
+ try {
101
+ entries = fs.readdirSync(dir, { withFileTypes: true });
102
+ } catch {
103
+ continue;
104
+ }
105
+
106
+ for (const ent of entries) {
107
+ const fullPath = path.join(dir, ent.name);
108
+ if (ent.isDirectory()) {
109
+ if (DEFAULT_IGNORES.has(ent.name)) continue;
110
+ queue.push({ dir: fullPath, depth: depth + 1 });
111
+ continue;
112
+ }
113
+ if (!ent.isFile()) continue;
114
+
115
+ visited += 1;
116
+ if (visited > maxFiles) return { results, truncated: true };
117
+ if (predicate(fullPath)) results.push(fullPath);
118
+ }
119
+ }
120
+
121
+ return { results, truncated: false };
122
+ }
123
+
124
+ function detectPluginHeaderFromPhpFile(filePath) {
125
+ const contents = readFileSafe(filePath, 128 * 1024);
126
+ if (!contents) return null;
127
+ const headerMatch = contents.match(/^\s*Plugin Name:\s*(.+)\s*$/im);
128
+ if (!headerMatch) return null;
129
+ return headerMatch[1].trim();
130
+ }
131
+
132
+ function detectThemeHeaderFromStyleCss(filePath) {
133
+ const contents = readFileSafe(filePath, 128 * 1024);
134
+ if (!contents) return null;
135
+ const headerMatch = contents.match(/^\s*Theme Name:\s*(.+)\s*$/im);
136
+ if (!headerMatch) return null;
137
+ return headerMatch[1].trim();
138
+ }
139
+
140
+ function guessWpCoreVersionFromCheckout(repoRoot) {
141
+ const versionPhp = path.join(repoRoot, "wp-includes", "version.php");
142
+ if (!existsFile(versionPhp)) return { value: null, source: null };
143
+ const contents = readFileSafe(versionPhp, 64 * 1024);
144
+ if (!contents) return { value: null, source: null };
145
+ const match = contents.match(/\$wp_version\s*=\s*'([^']+)'/);
146
+ if (!match) return { value: null, source: "wp-includes/version.php" };
147
+ return { value: match[1], source: "wp-includes/version.php" };
148
+ }
149
+
150
+ function guessGutenbergVersion(repoRoot) {
151
+ const gutenbergPackageJson = path.join(repoRoot, "packages", "plugins", "package.json");
152
+ const rootPackageJson = path.join(repoRoot, "package.json");
153
+
154
+ for (const candidate of [gutenbergPackageJson, rootPackageJson]) {
155
+ if (!existsFile(candidate)) continue;
156
+ const txt = readFileSafe(candidate);
157
+ if (!txt) continue;
158
+ try {
159
+ const pkg = JSON.parse(txt);
160
+ if (pkg?.name === "@wordpress/plugins" && typeof pkg?.version === "string") {
161
+ return { value: pkg.version, source: path.relative(repoRoot, candidate) };
162
+ }
163
+ if (pkg?.name === "gutenberg" && typeof pkg?.version === "string") {
164
+ return { value: pkg.version, source: path.relative(repoRoot, candidate) };
165
+ }
166
+ } catch {
167
+ // ignore
168
+ }
169
+ }
170
+ return { value: null, source: null };
171
+ }
172
+
173
+ function parsePackageJson(repoRoot) {
174
+ const p = path.join(repoRoot, "package.json");
175
+ if (!existsFile(p)) return null;
176
+ const txt = readFileSafe(p);
177
+ if (!txt) return null;
178
+ try {
179
+ return JSON.parse(txt);
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ function parseComposerJson(repoRoot) {
186
+ const p = path.join(repoRoot, "composer.json");
187
+ if (!existsFile(p)) return null;
188
+ const txt = readFileSafe(p);
189
+ if (!txt) return null;
190
+ try {
191
+ return JSON.parse(txt);
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
197
+ function detectConfigConstants(repoRoot) {
198
+ const { results: configFiles } = findFilesRecursive(repoRoot, (p) => path.basename(p) === "wp-config.php", {
199
+ maxFiles: 4000,
200
+ maxDepth: 4,
201
+ });
202
+ const configPath = configFiles[0] ?? null;
203
+ if (!configPath) {
204
+ return { source: null, constants: {} };
205
+ }
206
+
207
+ const contents = readFileSafe(configPath, 256 * 1024);
208
+ if (!contents) return { source: path.relative(repoRoot, configPath), constants: {} };
209
+
210
+ const c = contents;
211
+ const enabled = (name) =>
212
+ new RegExp(`define\\(\\s*['"]${name}['"]\\s*,\\s*(true|1)\\s*\\)`, "i").test(c) ||
213
+ new RegExp(`\\b${name}\\b\\s*=\\s*(true|1)`, "i").test(c);
214
+
215
+ const mentioned = (name) => new RegExp(`\\b${name}\\b`, "i").test(c);
216
+
217
+ return {
218
+ source: path.relative(repoRoot, configPath),
219
+ constants: {
220
+ savequeriesMentioned: mentioned("SAVEQUERIES"),
221
+ savequeriesEnabled: enabled("SAVEQUERIES"),
222
+ wpDebugMentioned: mentioned("WP_DEBUG"),
223
+ wpDebugEnabled: enabled("WP_DEBUG"),
224
+ disableWpCronMentioned: mentioned("DISABLE_WP_CRON"),
225
+ disableWpCronEnabled: enabled("DISABLE_WP_CRON"),
226
+ },
227
+ };
228
+ }
229
+
230
+ function detectKinds(repoRoot, signals) {
231
+ const kinds = new Set();
232
+
233
+ if (signals.isGutenbergRepo) kinds.add("gutenberg");
234
+ if (signals.isWpCoreCheckout) kinds.add("wp-core");
235
+ if (signals.hasWpContentDir) kinds.add("wp-site");
236
+ if (signals.detectedThemeName) kinds.add(signals.isBlockTheme ? "wp-block-theme" : "wp-theme");
237
+ if (signals.detectedPluginName) kinds.add(signals.isBlockPlugin ? "wp-block-plugin" : "wp-plugin");
238
+ if (signals.hasMuPluginsDir) kinds.add("wp-mu-plugin");
239
+
240
+ if (kinds.size === 0) kinds.add("unknown");
241
+
242
+ const priority = [
243
+ "gutenberg",
244
+ "wp-core",
245
+ "wp-site",
246
+ "wp-block-theme",
247
+ "wp-block-plugin",
248
+ "wp-theme",
249
+ "wp-mu-plugin",
250
+ "wp-plugin",
251
+ "unknown",
252
+ ];
253
+ let primary = "unknown";
254
+ for (const k of priority) {
255
+ if (kinds.has(k)) {
256
+ primary = k;
257
+ break;
258
+ }
259
+ }
260
+
261
+ return { kind: [...kinds], primary };
262
+ }
263
+
264
+ function buildRecommendations({ repoRoot, primaryKind, packageManager, packageJson, composerJson, tooling, signals }) {
265
+ const commands = [];
266
+ const notes = [];
267
+
268
+ if (tooling.node.hasPackageJson) {
269
+ const pm = packageManager ?? "npm";
270
+ const run = pm === "yarn" ? "yarn" : `${pm} run`;
271
+ const hasScript = (name) => Boolean(packageJson?.scripts && Object.prototype.hasOwnProperty.call(packageJson.scripts, name));
272
+ if (hasScript("lint")) commands.push(`${run} lint`);
273
+ if (hasScript("test")) commands.push(`${run} test`);
274
+ if (hasScript("build")) commands.push(`${run} build`);
275
+ if (hasScript("start")) commands.push(`${run} start`);
276
+ if (tooling.node.usesWordpressScripts) notes.push("Detected @wordpress/scripts usage; prefer its standard lint/build/test scripts.");
277
+ }
278
+
279
+ if (tooling.php.hasComposerJson) {
280
+ commands.push("composer install");
281
+ if (tooling.php.phpunitXml.length > 0) commands.push("vendor/bin/phpunit");
282
+ }
283
+
284
+ if (tooling.tests.hasWpEnv) notes.push("Detected wp-env; E2E workflows may rely on Docker.");
285
+ if (signals.scanTruncated) notes.push("Scan truncated due to file limit; some signals may be missing.");
286
+ if (primaryKind === "unknown") notes.push("Could not confidently classify repo; inspect root for plugin/theme headers or wp-content structure.");
287
+
288
+ return { commands, notes };
289
+ }
290
+
291
+ function main() {
292
+ const repoRoot = process.cwd();
293
+
294
+ const wpContent = path.join(repoRoot, "wp-content");
295
+ const pluginsDir = path.join(wpContent, "plugins");
296
+ const muPluginsDir = path.join(wpContent, "mu-plugins");
297
+ const themesDir = path.join(wpContent, "themes");
298
+
299
+ const isWpCoreCheckout = existsFile(path.join(repoRoot, "wp-includes", "version.php"));
300
+ const isGutenbergRepo =
301
+ existsDir(path.join(repoRoot, "packages")) &&
302
+ (existsDir(path.join(repoRoot, "packages", "block-editor")) || existsDir(path.join(repoRoot, "packages", "components")));
303
+
304
+ const packageJson = parsePackageJson(repoRoot);
305
+ const composerJson = parseComposerJson(repoRoot);
306
+ const packageManager = detectPackageManager(repoRoot);
307
+
308
+ const usesWordpressScripts = Boolean(
309
+ packageJson?.devDependencies?.["@wordpress/scripts"] ||
310
+ packageJson?.dependencies?.["@wordpress/scripts"] ||
311
+ packageJson?.scripts?.build?.includes("wp-scripts") ||
312
+ packageJson?.scripts?.start?.includes("wp-scripts") ||
313
+ packageJson?.scripts?.test?.includes("wp-scripts") ||
314
+ packageJson?.scripts?.lint?.includes("wp-scripts")
315
+ );
316
+
317
+ const pkgHasInteractivity = Boolean(
318
+ packageJson?.devDependencies?.["@wordpress/interactivity"] || packageJson?.dependencies?.["@wordpress/interactivity"]
319
+ );
320
+ const pkgHasAbilities = Boolean(
321
+ packageJson?.devDependencies?.["@wordpress/abilities"] || packageJson?.dependencies?.["@wordpress/abilities"]
322
+ );
323
+
324
+ const hasWpContentDir = existsDir(wpContent);
325
+ const hasPluginsDir = existsDir(pluginsDir);
326
+ const hasThemesDir = existsDir(themesDir);
327
+ const hasMuPluginsDir = existsDir(muPluginsDir);
328
+
329
+ const config = detectConfigConstants(repoRoot);
330
+
331
+ const pluginCandidates = [];
332
+ const themeCandidates = [];
333
+
334
+ // Root-level plugin/theme detection (common when repo root is the plugin/theme).
335
+ for (const entry of fs.readdirSync(repoRoot, { withFileTypes: true })) {
336
+ if (!entry.isFile()) continue;
337
+ if (entry.name.toLowerCase().endsWith(".php")) pluginCandidates.push(path.join(repoRoot, entry.name));
338
+ if (entry.name === "style.css") themeCandidates.push(path.join(repoRoot, entry.name));
339
+ }
340
+
341
+ let detectedPluginName = null;
342
+ for (const phpFile of pluginCandidates) {
343
+ detectedPluginName = detectPluginHeaderFromPhpFile(phpFile);
344
+ if (detectedPluginName) break;
345
+ }
346
+
347
+ let detectedThemeName = null;
348
+ for (const styleCss of themeCandidates) {
349
+ detectedThemeName = detectThemeHeaderFromStyleCss(styleCss);
350
+ if (detectedThemeName) break;
351
+ }
352
+
353
+ const { results: blockJsonFiles, truncated: scanTruncated } = findFilesRecursive(
354
+ repoRoot,
355
+ (p) => path.basename(p) === "block.json",
356
+ { maxFiles: 6000, maxDepth: 8 }
357
+ );
358
+ const { results: themeJsonFiles } = findFilesRecursive(repoRoot, (p) => path.basename(p) === "theme.json", {
359
+ maxFiles: 6000,
360
+ maxDepth: 8,
361
+ });
362
+
363
+ const templatesDirCandidates = [
364
+ path.join(repoRoot, "templates"),
365
+ path.join(repoRoot, "parts"),
366
+ path.join(repoRoot, "patterns"),
367
+ ];
368
+
369
+ const isBlockTheme = themeJsonFiles.length > 0 && templatesDirCandidates.some((p) => existsDir(p));
370
+ const isBlockPlugin = blockJsonFiles.length > 0;
371
+
372
+ const interactivityScan = scanForTokens(repoRoot, {
373
+ tokens: ["data-wp-interactive", "@wordpress/interactivity", "viewScriptModule"],
374
+ exts: [".php", ".js", ".ts", ".tsx", ".json", ".html"],
375
+ maxFiles: 2500,
376
+ maxDepth: 8,
377
+ });
378
+
379
+ const abilitiesScan = scanForTokens(repoRoot, {
380
+ tokens: [
381
+ "wp_register_ability(",
382
+ "wp_register_ability_category(",
383
+ "wp_abilities_api_init",
384
+ "wp_abilities_api_categories_init",
385
+ "wp-abilities/v1",
386
+ "@wordpress/abilities",
387
+ ],
388
+ exts: [".php", ".js", ".ts", ".tsx"],
389
+ maxFiles: 2500,
390
+ maxDepth: 8,
391
+ });
392
+
393
+ const innerBlocksScan = scanForTokens(repoRoot, {
394
+ tokens: ["InnerBlocks", "useInnerBlocksProps", "InnerBlocks.Content"],
395
+ exts: [".js", ".ts", ".tsx"],
396
+ maxFiles: 2500,
397
+ maxDepth: 8,
398
+ });
399
+
400
+ const wpCliConfigBasenames = new Set([
401
+ "wp-cli.yml",
402
+ "wp-cli.yaml",
403
+ "wp-cli.local.yml",
404
+ "wp-cli.local.yaml",
405
+ ".wp-cli.yml",
406
+ ".wp-cli.yaml",
407
+ ]);
408
+ const { results: wpCliConfigFiles, truncated: wpCliConfigTruncated } = findFilesRecursive(
409
+ repoRoot,
410
+ (p) => wpCliConfigBasenames.has(path.basename(p)),
411
+ { maxFiles: 6000, maxDepth: 6 }
412
+ );
413
+
414
+ const composerRequire = composerJson?.require && typeof composerJson.require === "object" ? composerJson.require : {};
415
+ const composerRequireDev =
416
+ composerJson?.["require-dev"] && typeof composerJson["require-dev"] === "object" ? composerJson["require-dev"] : {};
417
+ const composerHasWpCli = Boolean(
418
+ composerRequire["wp-cli/wp-cli"] ||
419
+ composerRequireDev["wp-cli/wp-cli"] ||
420
+ composerRequire["wp-cli/wp-cli-bundle"] ||
421
+ composerRequireDev["wp-cli/wp-cli-bundle"]
422
+ );
423
+
424
+ const wpCliTokenScan = scanForTokens(repoRoot, {
425
+ tokens: [
426
+ "wp search-replace",
427
+ "wp db export",
428
+ "wp db import",
429
+ "wp cron event",
430
+ "wp cache flush",
431
+ "wp rewrite flush",
432
+ "wp plugin update",
433
+ "wp theme update",
434
+ ],
435
+ exts: [".sh", ".yml", ".yaml", ".js", ".ts", ".php", ".json"],
436
+ maxFiles: 2500,
437
+ maxDepth: 8,
438
+ });
439
+
440
+ const usesInteractivityApi = pkgHasInteractivity || Object.keys(interactivityScan.matches).length > 0;
441
+ const usesAbilitiesApi = pkgHasAbilities || Object.keys(abilitiesScan.matches).length > 0;
442
+ const usesInnerBlocks = Object.keys(innerBlocksScan.matches).length > 0;
443
+ const usesWpCli = composerHasWpCli || wpCliConfigFiles.length > 0 || Object.keys(wpCliTokenScan.matches).length > 0;
444
+
445
+ const wpContentRoot = path.join(repoRoot, "wp-content");
446
+ const hasObjectCacheDropin = existsFile(path.join(wpContentRoot, "object-cache.php"));
447
+ const hasAdvancedCacheDropin = existsFile(path.join(wpContentRoot, "advanced-cache.php"));
448
+ const hasDbDropin = existsFile(path.join(wpContentRoot, "db.php"));
449
+ const hasSunriseDropin = existsFile(path.join(wpContentRoot, "sunrise.php"));
450
+ const hasQueryMonitorPlugin = existsDir(path.join(wpContentRoot, "plugins", "query-monitor"));
451
+ const hasPerformanceLabPlugin = existsDir(path.join(wpContentRoot, "plugins", "performance-lab"));
452
+
453
+ const phpunitXml = [];
454
+ for (const candidate of ["phpunit.xml", "phpunit.xml.dist"]) {
455
+ const full = path.join(repoRoot, candidate);
456
+ if (existsFile(full)) phpunitXml.push(candidate);
457
+ }
458
+
459
+ const hasWpEnv =
460
+ existsFile(path.join(repoRoot, ".wp-env.json")) ||
461
+ existsFile(path.join(repoRoot, ".wp-env.override.json")) ||
462
+ Boolean(packageJson?.devDependencies?.["@wordpress/env"] || packageJson?.dependencies?.["@wordpress/env"]);
463
+
464
+ const hasPlaywright = Boolean(
465
+ packageJson?.devDependencies?.["@playwright/test"] ||
466
+ packageJson?.dependencies?.["@playwright/test"] ||
467
+ packageJson?.devDependencies?.["@wordpress/e2e-test-utils-playwright"] ||
468
+ packageJson?.dependencies?.["@wordpress/e2e-test-utils-playwright"]
469
+ );
470
+
471
+ const hasJest = Boolean(
472
+ packageJson?.devDependencies?.jest ||
473
+ packageJson?.dependencies?.jest ||
474
+ packageJson?.devDependencies?.["@wordpress/jest-preset-default"] ||
475
+ packageJson?.dependencies?.["@wordpress/jest-preset-default"]
476
+ );
477
+
478
+ const hasPhpUnit = phpunitXml.length > 0 || Boolean(composerJson?.requireDev?.phpunit || composerJson?.["require-dev"]?.phpunit);
479
+
480
+ const signals = {
481
+ paths: {
482
+ repoRoot,
483
+ wpContent: hasWpContentDir ? wpContent : null,
484
+ pluginsDir: hasPluginsDir ? pluginsDir : null,
485
+ themesDir: hasThemesDir ? themesDir : null,
486
+ muPluginsDir: hasMuPluginsDir ? muPluginsDir : null,
487
+ },
488
+ isWpCoreCheckout,
489
+ isGutenbergRepo,
490
+ hasWpContentDir,
491
+ hasPluginsDir,
492
+ hasThemesDir,
493
+ hasMuPluginsDir,
494
+ detectedPluginName,
495
+ detectedThemeName,
496
+ isBlockPlugin,
497
+ isBlockTheme,
498
+ usesInteractivityApi,
499
+ usesAbilitiesApi,
500
+ usesInnerBlocks,
501
+ usesWpCli,
502
+ performanceHints: {
503
+ wpConfig: config.source,
504
+ constants: config.constants,
505
+ dropins: {
506
+ objectCache: hasObjectCacheDropin,
507
+ advancedCache: hasAdvancedCacheDropin,
508
+ db: hasDbDropin,
509
+ sunrise: hasSunriseDropin,
510
+ },
511
+ plugins: {
512
+ queryMonitor: hasQueryMonitorPlugin,
513
+ performanceLab: hasPerformanceLabPlugin,
514
+ },
515
+ },
516
+ interactivityHints: {
517
+ packageJson: pkgHasInteractivity,
518
+ matches: interactivityScan.matches,
519
+ scanTruncated: interactivityScan.truncated,
520
+ },
521
+ abilitiesHints: {
522
+ packageJson: pkgHasAbilities,
523
+ matches: abilitiesScan.matches,
524
+ scanTruncated: abilitiesScan.truncated,
525
+ },
526
+ innerBlocksHints: {
527
+ matches: innerBlocksScan.matches,
528
+ scanTruncated: innerBlocksScan.truncated,
529
+ },
530
+ wpCliHints: {
531
+ configFiles: wpCliConfigFiles.map((p) => path.relative(repoRoot, p)).slice(0, 50),
532
+ configScanTruncated: wpCliConfigTruncated,
533
+ composerJson: composerHasWpCli,
534
+ matches: wpCliTokenScan.matches,
535
+ scanTruncated: wpCliTokenScan.truncated,
536
+ },
537
+ blockJsonFiles: blockJsonFiles.map((p) => path.relative(repoRoot, p)).slice(0, 50),
538
+ themeJsonFiles: themeJsonFiles.map((p) => path.relative(repoRoot, p)).slice(0, 50),
539
+ scanTruncated,
540
+ };
541
+
542
+ const { kind, primary } = detectKinds(repoRoot, signals);
543
+
544
+ const versions = {
545
+ wordpress: {
546
+ core: guessWpCoreVersionFromCheckout(repoRoot),
547
+ },
548
+ gutenberg: guessGutenbergVersion(repoRoot),
549
+ };
550
+
551
+ const tooling = {
552
+ php: {
553
+ hasComposerJson: existsFile(path.join(repoRoot, "composer.json")),
554
+ hasVendorDir: existsDir(path.join(repoRoot, "vendor")),
555
+ phpunitXml,
556
+ },
557
+ node: {
558
+ hasPackageJson: existsFile(path.join(repoRoot, "package.json")),
559
+ packageManager,
560
+ usesWordpressScripts,
561
+ },
562
+ tests: {
563
+ hasPhpUnit,
564
+ hasWpEnv,
565
+ hasPlaywright,
566
+ hasJest,
567
+ },
568
+ };
569
+
570
+ const recommendations = buildRecommendations({
571
+ repoRoot,
572
+ primaryKind: primary,
573
+ packageManager,
574
+ packageJson,
575
+ composerJson,
576
+ tooling,
577
+ signals,
578
+ });
579
+
580
+ const report = {
581
+ tool: { name: "detect_wp_project", version: TOOL_VERSION },
582
+ project: { kind, primary, notes: [] },
583
+ signals,
584
+ tooling,
585
+ versions,
586
+ recommendations,
587
+ };
588
+
589
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
590
+ }
591
+
592
+ main();
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: wp-rest-api
3
+ description: "Use when building, extending, or debugging WordPress REST API endpoints/routes: register_rest_route, WP_REST_Controller/controller classes, schema/argument validation, permission_callback/authentication, response shaping, register_rest_field/register_meta, or exposing CPTs/taxonomies via show_in_rest."
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 REST API
8
+
9
+ ## When to use
10
+
11
+ Use this skill when you need to:
12
+
13
+ - create or update REST routes/endpoints
14
+ - debug 401/403/404 errors or permission/nonce issues
15
+ - add custom fields/meta to REST responses
16
+ - expose custom post types or taxonomies via REST
17
+ - implement schema + argument validation
18
+ - adjust response links/embedding/pagination
19
+
20
+ ## Inputs required
21
+
22
+ - Repo root + target plugin/theme/mu-plugin (path to entrypoint).
23
+ - Desired namespace + version (e.g. `my-plugin/v1`) and routes.
24
+ - Authentication mode (cookie + nonce vs application passwords vs auth plugin).
25
+ - Target WordPress version constraints (if below 6.9, call out).
26
+
27
+ ## Procedure
28
+
29
+ ### 0) Triage and locate REST usage
30
+
31
+ 1. Run triage:
32
+ - `node skills/wp-project-triage/scripts/detect_wp_project.mjs`
33
+ 2. Search for existing REST usage:
34
+ - `register_rest_route`
35
+ - `WP_REST_Controller`
36
+ - `rest_api_init`
37
+ - `show_in_rest`, `rest_base`, `rest_controller_class`
38
+
39
+ If this is a full site repo, pick the specific plugin/theme before changing code.
40
+
41
+ ### 1) Choose the right approach
42
+
43
+ - **Expose CPT/taxonomy in `wp/v2`:**
44
+ - Use `show_in_rest => true` + `rest_base` if needed.
45
+ - Optionally provide `rest_controller_class`.
46
+ - Read `references/custom-content-types.md`.
47
+ - **Custom endpoints:**
48
+ - Use `register_rest_route()` on `rest_api_init`.
49
+ - Prefer a controller class (`WP_REST_Controller` subclass) for anything non-trivial.
50
+ - Read `references/routes-and-endpoints.md` and `references/schema.md`.
51
+
52
+ ### 2) Register routes safely (namespaces, methods, permissions)
53
+
54
+ - Use a unique namespace `vendor/v1`; avoid `wp/*` unless core.
55
+ - Always provide `permission_callback` (use `__return_true` for public endpoints).
56
+ - Use `WP_REST_Server::READABLE/CREATABLE/EDITABLE/DELETABLE` constants.
57
+ - Return data via `rest_ensure_response()` or `WP_REST_Response`.
58
+ - Return errors via `WP_Error` with an explicit `status`.
59
+
60
+ Read `references/routes-and-endpoints.md`.
61
+
62
+ ### 3) Validate/sanitize request args
63
+
64
+ - Define `args` with `type`, `default`, `required`, `validate_callback`, `sanitize_callback`.
65
+ - Prefer JSON Schema validation with `rest_validate_value_from_schema` then `rest_sanitize_value_from_schema`.
66
+ - Never read `$_GET`/`$_POST` directly inside endpoints; use `WP_REST_Request`.
67
+
68
+ Read `references/schema.md`.
69
+
70
+ ### 4) Responses, fields, and links
71
+
72
+ - Do **not** remove core fields from default endpoints; add fields instead.
73
+ - Use `register_rest_field` for computed fields; `register_meta` with `show_in_rest` for meta.
74
+ - For `object`/`array` meta, define schema in `show_in_rest.schema`.
75
+ - If you need unfiltered post content (e.g., ToC plugins injecting HTML), request `?context=edit` to access `content.raw` (auth required). Pair with `_fields=content.raw` to keep responses small.
76
+ - Add related resource links via `WP_REST_Response::add_link()`.
77
+
78
+ Read `references/responses-and-fields.md`.
79
+
80
+ ### 5) Authentication and authorization
81
+
82
+ - For wp-admin/JS: cookie auth + `X-WP-Nonce` (action `wp_rest`).
83
+ - For external clients: application passwords (basic auth) or an auth plugin.
84
+ - Use capability checks in `permission_callback` (authorization), not just “logged in”.
85
+
86
+ Read `references/authentication.md`.
87
+
88
+ ### 6) Client-facing behavior (discovery, pagination, embeds)
89
+
90
+ - Ensure discovery works (`Link` header or `<link rel="https://api.w.org/">`).
91
+ - Support `_fields`, `_embed`, `_method`, `_envelope`, pagination headers.
92
+ - Remember `per_page` is capped at 100.
93
+
94
+ Read `references/discovery-and-params.md`.
95
+
96
+ ## Verification
97
+
98
+ - `/wp-json/` index includes your namespace.
99
+ - `OPTIONS` on your route returns schema (when provided).
100
+ - Endpoint returns expected data; permission failures return 401/403 as appropriate.
101
+ - CPT/taxonomy routes appear under `wp/v2` when `show_in_rest` is true.
102
+ - Run repo lint/tests and any PHP/JS build steps.
103
+
104
+ ## Failure modes / debugging
105
+
106
+ - 404: `rest_api_init` not firing, route typo, or permalinks off (use `?rest_route=`).
107
+ - 401/403: missing nonce/auth, or `permission_callback` too strict.
108
+ - `_doing_it_wrong` for missing `permission_callback`: add it (use `__return_true` if public).
109
+ - Invalid params: missing/incorrect `args` schema or validation callbacks.
110
+ - Fields missing: `show_in_rest` false, meta not registered, or CPT lacks `custom-fields` support.
111
+
112
+ ## Escalation
113
+
114
+ If version support or behavior is unclear, consult the REST API Handbook and core docs before inventing patterns.