claude-plugin-wordpress-manager 1.5.0 → 1.7.1

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 (68) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +97 -0
  3. package/README.md +27 -13
  4. package/agents/wp-accessibility-auditor.md +206 -0
  5. package/agents/wp-content-strategist.md +18 -0
  6. package/agents/wp-deployment-engineer.md +34 -2
  7. package/agents/wp-performance-optimizer.md +12 -0
  8. package/agents/wp-security-auditor.md +20 -0
  9. package/agents/wp-security-hardener.md +266 -0
  10. package/agents/wp-site-manager.md +14 -0
  11. package/agents/wp-test-engineer.md +207 -0
  12. package/docs/guides/INDEX.md +46 -0
  13. package/docs/guides/wp-blog.md +590 -0
  14. package/docs/guides/wp-design-system.md +976 -0
  15. package/docs/guides/wp-ecommerce.md +786 -0
  16. package/docs/guides/wp-landing-page.md +762 -0
  17. package/docs/guides/wp-portfolio.md +713 -0
  18. package/docs/plans/2026-02-27-design-system-guide-design.md +30 -0
  19. package/docs/plans/2026-02-27-site-type-guides-design.md +44 -0
  20. package/package.json +2 -2
  21. package/skills/wordpress-router/references/decision-tree.md +12 -2
  22. package/skills/wp-accessibility/SKILL.md +170 -0
  23. package/skills/wp-accessibility/references/a11y-audit-tools.md +248 -0
  24. package/skills/wp-accessibility/references/a11y-testing.md +222 -0
  25. package/skills/wp-accessibility/references/block-a11y.md +247 -0
  26. package/skills/wp-accessibility/references/interactive-a11y.md +272 -0
  27. package/skills/wp-accessibility/references/media-a11y.md +254 -0
  28. package/skills/wp-accessibility/references/theme-a11y.md +309 -0
  29. package/skills/wp-audit/SKILL.md +4 -0
  30. package/skills/wp-block-development/SKILL.md +5 -0
  31. package/skills/wp-block-themes/SKILL.md +4 -0
  32. package/skills/wp-e2e-testing/SKILL.md +186 -0
  33. package/skills/wp-e2e-testing/references/ci-integration.md +174 -0
  34. package/skills/wp-e2e-testing/references/jest-wordpress.md +114 -0
  35. package/skills/wp-e2e-testing/references/phpunit-wordpress.md +141 -0
  36. package/skills/wp-e2e-testing/references/playwright-wordpress.md +108 -0
  37. package/skills/wp-e2e-testing/references/test-data-generation.md +127 -0
  38. package/skills/wp-e2e-testing/references/visual-regression.md +107 -0
  39. package/skills/wp-e2e-testing/references/wp-env-setup.md +97 -0
  40. package/skills/wp-e2e-testing/scripts/test_inspect.mjs +375 -0
  41. package/skills/wp-headless/SKILL.md +168 -0
  42. package/skills/wp-headless/references/api-layer-choice.md +160 -0
  43. package/skills/wp-headless/references/cors-config.md +245 -0
  44. package/skills/wp-headless/references/frontend-integration.md +331 -0
  45. package/skills/wp-headless/references/headless-auth.md +286 -0
  46. package/skills/wp-headless/references/webhooks.md +277 -0
  47. package/skills/wp-headless/references/wpgraphql.md +331 -0
  48. package/skills/wp-headless/scripts/headless_inspect.mjs +321 -0
  49. package/skills/wp-i18n/SKILL.md +170 -0
  50. package/skills/wp-i18n/references/js-i18n.md +201 -0
  51. package/skills/wp-i18n/references/multilingual-setup.md +219 -0
  52. package/skills/wp-i18n/references/php-i18n.md +196 -0
  53. package/skills/wp-i18n/references/rtl-support.md +206 -0
  54. package/skills/wp-i18n/references/translation-workflow.md +178 -0
  55. package/skills/wp-i18n/references/wpcli-i18n.md +177 -0
  56. package/skills/wp-i18n/scripts/i18n_inspect.mjs +330 -0
  57. package/skills/wp-interactivity-api/SKILL.md +4 -0
  58. package/skills/wp-plugin-development/SKILL.md +6 -0
  59. package/skills/wp-rest-api/SKILL.md +4 -0
  60. package/skills/wp-security/SKILL.md +179 -0
  61. package/skills/wp-security/references/api-restriction.md +147 -0
  62. package/skills/wp-security/references/authentication-hardening.md +105 -0
  63. package/skills/wp-security/references/filesystem-hardening.md +105 -0
  64. package/skills/wp-security/references/http-headers.md +105 -0
  65. package/skills/wp-security/references/incident-response.md +144 -0
  66. package/skills/wp-security/references/user-capabilities.md +115 -0
  67. package/skills/wp-security/references/wp-config-security.md +129 -0
  68. package/skills/wp-security/scripts/security_inspect.mjs +393 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * security_inspect.mjs — Detect WordPress security configuration and potential issues.
3
+ *
4
+ * Scans wp-config.php constants, file permissions, debug settings,
5
+ * .htaccess hardening, and security plugin presence.
6
+ * Outputs a JSON report to stdout.
7
+ *
8
+ * Usage:
9
+ * node security_inspect.mjs [--cwd=/path/to/wordpress]
10
+ *
11
+ * Exit codes:
12
+ * 0 — WordPress installation found and scanned
13
+ * 1 — no WordPress installation found
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import process from "node:process";
19
+ import { execSync } from "node:child_process";
20
+
21
+ const TOOL_VERSION = "1.0.0";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function statSafe(p) {
28
+ try {
29
+ return fs.statSync(p);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function readFileSafe(p) {
36
+ try {
37
+ return fs.readFileSync(p, "utf8");
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function readJsonSafe(p) {
44
+ const raw = readFileSafe(p);
45
+ if (!raw) return null;
46
+ try {
47
+ return JSON.parse(raw);
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function execSafe(cmd, cwd, timeoutMs = 5000) {
54
+ try {
55
+ return execSync(cmd, { encoding: "utf8", timeout: timeoutMs, cwd, stdio: ["pipe", "pipe", "pipe"] }).trim();
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Parse --cwd argument
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function parseCwd() {
66
+ const cwdArg = process.argv.find((a) => a.startsWith("--cwd="));
67
+ return cwdArg ? cwdArg.slice(6) : process.cwd();
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Detect WordPress installation
72
+ // ---------------------------------------------------------------------------
73
+
74
+ function findWpRoot(cwd) {
75
+ // Check common locations
76
+ const candidates = [
77
+ cwd,
78
+ path.join(cwd, "wordpress"),
79
+ path.join(cwd, "wp"),
80
+ path.join(cwd, "public_html"),
81
+ path.join(cwd, "htdocs"),
82
+ ];
83
+
84
+ for (const dir of candidates) {
85
+ if (statSafe(path.join(dir, "wp-config.php"))?.isFile()) return dir;
86
+ if (statSafe(path.join(dir, "wp-includes", "version.php"))?.isFile()) return dir;
87
+ }
88
+
89
+ // wp-config.php one level up (common security practice)
90
+ const parent = path.dirname(cwd);
91
+ if (statSafe(path.join(parent, "wp-config.php"))?.isFile()) {
92
+ return cwd;
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Scan wp-config.php
100
+ // ---------------------------------------------------------------------------
101
+
102
+ function scanWpConfig(wpRoot) {
103
+ const result = {
104
+ found: false,
105
+ location: null,
106
+ constants: {},
107
+ issues: [],
108
+ };
109
+
110
+ // Check current dir and parent
111
+ let configPath = path.join(wpRoot, "wp-config.php");
112
+ if (!statSafe(configPath)?.isFile()) {
113
+ configPath = path.join(path.dirname(wpRoot), "wp-config.php");
114
+ }
115
+ if (!statSafe(configPath)?.isFile()) return result;
116
+
117
+ result.found = true;
118
+ result.location = configPath;
119
+
120
+ const content = readFileSafe(configPath);
121
+ if (!content) return result;
122
+
123
+ // Extract defined constants
124
+ const constantPattern = /define\s*\(\s*['"](\w+)['"]\s*,\s*(.+?)\s*\)/g;
125
+ let match;
126
+ while ((match = constantPattern.exec(content)) !== null) {
127
+ const name = match[1];
128
+ const rawValue = match[2].trim().replace(/['"]/g, "").replace(/\);$/, "");
129
+ result.constants[name] = rawValue;
130
+ }
131
+
132
+ // Check security constants
133
+ const securityConstants = {
134
+ WP_DEBUG: { safe: "false", risk: "Exposes error details in production" },
135
+ WP_DEBUG_DISPLAY: { safe: "false", risk: "Shows errors to visitors" },
136
+ WP_DEBUG_LOG: { safe: "false", risk: "Log file may be publicly accessible" },
137
+ DISALLOW_FILE_EDIT: { safe: "true", risk: "Theme/plugin editor is enabled" },
138
+ FORCE_SSL_ADMIN: { safe: "true", risk: "Admin not forced to HTTPS" },
139
+ };
140
+
141
+ for (const [name, check] of Object.entries(securityConstants)) {
142
+ const value = result.constants[name];
143
+ if (value !== undefined && value.toLowerCase() !== check.safe) {
144
+ result.issues.push({ constant: name, value, risk: check.risk });
145
+ }
146
+ if (value === undefined && name === "DISALLOW_FILE_EDIT") {
147
+ result.issues.push({ constant: name, value: "not set", risk: "File editor not explicitly disabled" });
148
+ }
149
+ if (value === undefined && name === "FORCE_SSL_ADMIN") {
150
+ result.issues.push({ constant: name, value: "not set", risk: "SSL not forced for admin" });
151
+ }
152
+ }
153
+
154
+ // Check security keys for defaults
155
+ const keyNames = ["AUTH_KEY", "SECURE_AUTH_KEY", "LOGGED_IN_KEY", "NONCE_KEY"];
156
+ for (const key of keyNames) {
157
+ const val = result.constants[key];
158
+ if (val && val.includes("put your unique phrase here")) {
159
+ result.issues.push({ constant: key, value: "default", risk: "Security key not changed from default" });
160
+ }
161
+ }
162
+
163
+ // Check table prefix
164
+ const prefixMatch = content.match(/\$table_prefix\s*=\s*['"](\w+)['"]/);
165
+ if (prefixMatch) {
166
+ result.constants._table_prefix = prefixMatch[1];
167
+ if (prefixMatch[1] === "wp_") {
168
+ result.issues.push({ constant: "$table_prefix", value: "wp_", risk: "Default table prefix" });
169
+ }
170
+ }
171
+
172
+ return result;
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Scan file permissions
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function scanPermissions(wpRoot) {
180
+ const result = { checks: [], issues: [] };
181
+
182
+ if (process.platform === "win32") {
183
+ result.checks.push({ file: "N/A", note: "Permission checks not available on Windows" });
184
+ return result;
185
+ }
186
+
187
+ const fileChecks = [
188
+ { path: "wp-config.php", maxMode: 0o440, label: "wp-config.php" },
189
+ { path: ".htaccess", maxMode: 0o644, label: ".htaccess" },
190
+ { path: "wp-content", maxMode: 0o755, label: "wp-content/" },
191
+ { path: "wp-content/uploads", maxMode: 0o755, label: "wp-content/uploads/" },
192
+ ];
193
+
194
+ for (const check of fileChecks) {
195
+ const full = path.join(wpRoot, check.path);
196
+ const stat = statSafe(full);
197
+ if (!stat) {
198
+ result.checks.push({ file: check.label, exists: false });
199
+ continue;
200
+ }
201
+
202
+ const mode = stat.mode & 0o777;
203
+ const modeStr = "0" + mode.toString(8);
204
+ const isWorldWritable = (mode & 0o002) !== 0;
205
+
206
+ result.checks.push({
207
+ file: check.label,
208
+ exists: true,
209
+ mode: modeStr,
210
+ worldWritable: isWorldWritable,
211
+ });
212
+
213
+ if (isWorldWritable) {
214
+ result.issues.push({ file: check.label, mode: modeStr, risk: "World-writable" });
215
+ }
216
+
217
+ if (check.label === "wp-config.php" && mode > check.maxMode) {
218
+ result.issues.push({
219
+ file: check.label,
220
+ mode: modeStr,
221
+ recommended: "0" + check.maxMode.toString(8),
222
+ risk: "wp-config.php permissions too open",
223
+ });
224
+ }
225
+ }
226
+
227
+ // Check for PHP files in uploads
228
+ const uploadsDir = path.join(wpRoot, "wp-content", "uploads");
229
+ if (statSafe(uploadsDir)?.isDirectory()) {
230
+ const phpInUploads = execSafe(
231
+ `find "${uploadsDir}" -name "*.php" -type f 2>/dev/null | head -5`,
232
+ wpRoot
233
+ );
234
+ if (phpInUploads && phpInUploads.length > 0) {
235
+ result.issues.push({
236
+ file: "wp-content/uploads/",
237
+ risk: "PHP files found in uploads directory",
238
+ files: phpInUploads.split("\n").map((f) => path.relative(wpRoot, f)),
239
+ });
240
+ }
241
+ }
242
+
243
+ return result;
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Scan .htaccess
248
+ // ---------------------------------------------------------------------------
249
+
250
+ function scanHtaccess(wpRoot) {
251
+ const result = { found: false, hardening: [], missing: [] };
252
+
253
+ const htaccessPath = path.join(wpRoot, ".htaccess");
254
+ const content = readFileSafe(htaccessPath);
255
+ if (!content) return result;
256
+
257
+ result.found = true;
258
+
259
+ const hardeningChecks = [
260
+ { name: "xmlrpc-blocked", pattern: /xmlrpc\.php/i },
261
+ { name: "directory-listing-disabled", pattern: /Options\s+-Indexes/i },
262
+ { name: "wp-config-protected", pattern: /wp-config\.php/i },
263
+ { name: "server-signature-off", pattern: /ServerSignature\s+Off/i },
264
+ { name: "file-type-restriction", pattern: /FilesMatch.*\.(php|phtml)/i },
265
+ ];
266
+
267
+ for (const check of hardeningChecks) {
268
+ if (check.pattern.test(content)) {
269
+ result.hardening.push(check.name);
270
+ } else {
271
+ result.missing.push(check.name);
272
+ }
273
+ }
274
+
275
+ return result;
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Scan security plugins
280
+ // ---------------------------------------------------------------------------
281
+
282
+ function scanSecurityPlugins(wpRoot) {
283
+ const pluginsDir = path.join(wpRoot, "wp-content", "plugins");
284
+ if (!statSafe(pluginsDir)?.isDirectory()) return { detected: [] };
285
+
286
+ const securityPlugins = [
287
+ { dir: "wordfence", name: "Wordfence Security" },
288
+ { dir: "sucuri-scanner", name: "Sucuri Security" },
289
+ { dir: "ithemes-security-pro", name: "iThemes Security Pro" },
290
+ { dir: "better-wp-security", name: "iThemes Security" },
291
+ { dir: "all-in-one-wp-security-and-firewall", name: "All In One WP Security" },
292
+ { dir: "wp-cerber", name: "WP Cerber Security" },
293
+ { dir: "limit-login-attempts-reloaded", name: "Limit Login Attempts Reloaded" },
294
+ { dir: "two-factor", name: "Two Factor Authentication" },
295
+ { dir: "disable-xml-rpc", name: "Disable XML-RPC" },
296
+ ];
297
+
298
+ const detected = [];
299
+ for (const plugin of securityPlugins) {
300
+ if (statSafe(path.join(pluginsDir, plugin.dir))?.isDirectory()) {
301
+ detected.push(plugin.name);
302
+ }
303
+ }
304
+
305
+ return { detected };
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Scan HTTP headers (via WP-CLI if available)
310
+ // ---------------------------------------------------------------------------
311
+
312
+ function scanHeaders(wpRoot) {
313
+ // Only possible if site is running — skip if no WP-CLI
314
+ const wpCli = execSafe("command -v wp", wpRoot);
315
+ if (!wpCli) return { available: false };
316
+
317
+ const siteUrl = execSafe("wp option get siteurl --skip-themes --skip-plugins 2>/dev/null", wpRoot);
318
+ if (!siteUrl) return { available: false };
319
+
320
+ // Try to fetch headers
321
+ const headers = execSafe(`curl -sI -o /dev/null -w '%{http_code}' "${siteUrl}" 2>/dev/null`, wpRoot);
322
+ return { available: true, siteUrl, reachable: headers === "200" || headers === "301" || headers === "302" };
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Main
327
+ // ---------------------------------------------------------------------------
328
+
329
+ function main() {
330
+ const cwd = parseCwd();
331
+
332
+ if (!statSafe(cwd)?.isDirectory()) {
333
+ console.error(`Error: directory not found: ${cwd}`);
334
+ process.exit(1);
335
+ }
336
+
337
+ const wpRoot = findWpRoot(cwd);
338
+ if (!wpRoot) {
339
+ console.log(JSON.stringify({
340
+ tool: "security_inspect",
341
+ version: TOOL_VERSION,
342
+ cwd,
343
+ detected: false,
344
+ error: "No WordPress installation found",
345
+ }, null, 2));
346
+ process.exit(1);
347
+ }
348
+
349
+ const wpConfig = scanWpConfig(wpRoot);
350
+ const permissions = scanPermissions(wpRoot);
351
+ const htaccess = scanHtaccess(wpRoot);
352
+ const plugins = scanSecurityPlugins(wpRoot);
353
+ const headers = scanHeaders(wpRoot);
354
+
355
+ const totalIssues = wpConfig.issues.length + permissions.issues.length + htaccess.missing.length;
356
+
357
+ const report = {
358
+ tool: "security_inspect",
359
+ version: TOOL_VERSION,
360
+ cwd,
361
+ wpRoot,
362
+ detected: true,
363
+ summary: {
364
+ totalIssues,
365
+ severity: totalIssues === 0 ? "good" : totalIssues <= 3 ? "moderate" : "needs-attention",
366
+ },
367
+ wpConfig,
368
+ permissions,
369
+ htaccess,
370
+ securityPlugins: plugins,
371
+ headers,
372
+ recommendations: [],
373
+ };
374
+
375
+ // Generate recommendations
376
+ if (plugins.detected.length === 0) {
377
+ report.recommendations.push("No security plugin detected. Consider installing Wordfence or Sucuri.");
378
+ }
379
+ if (wpConfig.issues.length > 0) {
380
+ report.recommendations.push(`${wpConfig.issues.length} wp-config.php issue(s) found. Review security constants.`);
381
+ }
382
+ if (htaccess.missing.length > 0) {
383
+ report.recommendations.push(`Missing .htaccess hardening: ${htaccess.missing.join(", ")}`);
384
+ }
385
+ if (permissions.issues.length > 0) {
386
+ report.recommendations.push(`${permissions.issues.length} file permission issue(s) found.`);
387
+ }
388
+
389
+ console.log(JSON.stringify(report, null, 2));
390
+ process.exit(0);
391
+ }
392
+
393
+ main();