wp-skills 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/LICENSE +16 -0
- package/README.md +157 -0
- package/docs/ai-authorship.md +60 -0
- package/docs/authoring-guide.md +56 -0
- package/docs/compatibility-policy.md +18 -0
- package/docs/packaging.md +35 -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 +21 -0
- package/shared/scripts/ai-generate-updates.mjs +458 -0
- package/shared/scripts/scaffold-skill.mjs +62 -0
- package/shared/scripts/skillpack-build.mjs +168 -0
- package/shared/scripts/skillpack-cli.mjs +77 -0
- package/shared/scripts/skillpack-install.mjs +338 -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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const buildScript = path.join(scriptDir, "skillpack-build.mjs");
|
|
8
|
+
const installScript = path.join(scriptDir, "skillpack-install.mjs");
|
|
9
|
+
const packageRoot = path.resolve(scriptDir, "..", "..");
|
|
10
|
+
|
|
11
|
+
function printUsage() {
|
|
12
|
+
process.stderr.write(
|
|
13
|
+
[
|
|
14
|
+
"Usage:",
|
|
15
|
+
" wp-skills <command> [options]",
|
|
16
|
+
"",
|
|
17
|
+
"Commands:",
|
|
18
|
+
" install Install skills into a target repository (default command)",
|
|
19
|
+
" list List available skills",
|
|
20
|
+
" build Build dist layout for selected targets",
|
|
21
|
+
"",
|
|
22
|
+
"Examples:",
|
|
23
|
+
" npx wp-skills install --dest=. --targets=codex,vscode,claude,cursor,antigravity",
|
|
24
|
+
" npx wp-skills list",
|
|
25
|
+
" npx wp-skills build --out=dist --clean",
|
|
26
|
+
"",
|
|
27
|
+
].join("\n")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runNode(scriptPath, args) {
|
|
32
|
+
const result = spawnSync(process.execPath, [scriptPath, ...args], {
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (result.error) {
|
|
37
|
+
throw result.error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.exit(result.status ?? 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const argv = process.argv.slice(2);
|
|
44
|
+
if (argv.length === 0) {
|
|
45
|
+
runNode(installScript, [`--from=${packageRoot}`]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const [command, ...rest] = argv;
|
|
49
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
50
|
+
printUsage();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (command === "build") {
|
|
55
|
+
runNode(buildScript, rest);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (command === "install") {
|
|
59
|
+
const args = rest.some((a) => a.startsWith("--from=")) ? rest : [`--from=${packageRoot}`, ...rest];
|
|
60
|
+
runNode(installScript, args);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command === "list") {
|
|
64
|
+
const args = rest.some((a) => a.startsWith("--from=")) ? rest : [`--from=${packageRoot}`, ...rest];
|
|
65
|
+
runNode(installScript, ["--list", ...args]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const looksLikeFlag = command.startsWith("-");
|
|
69
|
+
if (looksLikeFlag) {
|
|
70
|
+
// Backward-compatible shorthand: treat bare flags as install args.
|
|
71
|
+
const args = argv.some((a) => a.startsWith("--from=")) ? argv : [`--from=${packageRoot}`, ...argv];
|
|
72
|
+
runNode(installScript, args);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
76
|
+
printUsage();
|
|
77
|
+
process.exit(2);
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const DEFAULT_FROM = path.resolve(SCRIPT_DIR, "..", "..");
|
|
8
|
+
|
|
9
|
+
function usage() {
|
|
10
|
+
process.stderr.write(
|
|
11
|
+
[
|
|
12
|
+
"Usage:",
|
|
13
|
+
" node shared/scripts/skillpack-install.mjs --dest=<repo-root> [options]",
|
|
14
|
+
"",
|
|
15
|
+
"Options:",
|
|
16
|
+
" --dest=<path> Destination repo root (required, unless using --global)",
|
|
17
|
+
" --from=<path> Source directory (default: package root, supports source or dist layout)",
|
|
18
|
+
" --targets=<list> Comma-separated targets: codex, vscode, claude, claude-global, cursor, cursor-global, antigravity (default: codex,vscode)",
|
|
19
|
+
" --skills=<list> Comma-separated skill names to install (default: all)",
|
|
20
|
+
" --mode=<mode> 'replace' (default) or 'merge'",
|
|
21
|
+
" --global Shorthand for --targets=claude-global (installs to ~/.claude/skills)",
|
|
22
|
+
" --dry-run Show what would be installed without making changes",
|
|
23
|
+
" --list List available skills and exit",
|
|
24
|
+
"",
|
|
25
|
+
"Targets:",
|
|
26
|
+
" codex Install to <dest>/.codex/skills/",
|
|
27
|
+
" vscode Install to <dest>/.github/skills/",
|
|
28
|
+
" claude Install to <dest>/.claude/skills/ (project-level)",
|
|
29
|
+
" claude-global Install to ~/.claude/skills/ (user-level, ignores --dest)",
|
|
30
|
+
" cursor Install to <dest>/.cursor/skills/",
|
|
31
|
+
" cursor-global Install to ~/.cursor/skills/ (user-level, ignores --dest)",
|
|
32
|
+
" antigravity Install to <dest>/.agent/skill/",
|
|
33
|
+
"",
|
|
34
|
+
"Examples:",
|
|
35
|
+
" # Install directly from source layout",
|
|
36
|
+
" node shared/scripts/skillpack-install.mjs --dest=../my-wp-repo --targets=codex,vscode,claude,cursor,antigravity",
|
|
37
|
+
"",
|
|
38
|
+
" # Install from dist layout",
|
|
39
|
+
" node shared/scripts/skillpack-build.mjs --clean",
|
|
40
|
+
" node shared/scripts/skillpack-install.mjs --from=dist --dest=../my-wp-repo --targets=codex,vscode,claude,cursor,antigravity",
|
|
41
|
+
"",
|
|
42
|
+
" # Install globally for Claude Code (all skills)",
|
|
43
|
+
" node shared/scripts/skillpack-install.mjs --global",
|
|
44
|
+
"",
|
|
45
|
+
" # Install globally for Cursor (all skills)",
|
|
46
|
+
" node shared/scripts/skillpack-install.mjs --targets=cursor-global",
|
|
47
|
+
"",
|
|
48
|
+
" # Install specific skills globally",
|
|
49
|
+
" node shared/scripts/skillpack-install.mjs --global --skills=wp-playground,wp-block-development",
|
|
50
|
+
"",
|
|
51
|
+
" # Install to project with specific skills",
|
|
52
|
+
" node shared/scripts/skillpack-install.mjs --dest=../my-repo --targets=claude,cursor --skills=wp-wpcli-and-ops",
|
|
53
|
+
"",
|
|
54
|
+
].join("\n")
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(argv) {
|
|
59
|
+
const args = {
|
|
60
|
+
from: DEFAULT_FROM,
|
|
61
|
+
dest: null,
|
|
62
|
+
targets: ["codex", "vscode"],
|
|
63
|
+
skills: [],
|
|
64
|
+
mode: "replace",
|
|
65
|
+
dryRun: false,
|
|
66
|
+
global: false,
|
|
67
|
+
list: false,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const a of argv) {
|
|
71
|
+
if (a === "--help" || a === "-h") args.help = true;
|
|
72
|
+
else if (a === "--dry-run") args.dryRun = true;
|
|
73
|
+
else if (a === "--global") args.global = true;
|
|
74
|
+
else if (a === "--list") args.list = true;
|
|
75
|
+
else if (a.startsWith("--from=")) args.from = a.slice("--from=".length);
|
|
76
|
+
else if (a.startsWith("--dest=")) args.dest = a.slice("--dest=".length);
|
|
77
|
+
else if (a.startsWith("--targets=")) args.targets = a.slice("--targets=".length).split(",").filter(Boolean);
|
|
78
|
+
else if (a.startsWith("--skills=")) args.skills = a.slice("--skills=".length).split(",").filter(Boolean);
|
|
79
|
+
else if (a.startsWith("--mode=")) args.mode = a.slice("--mode=".length);
|
|
80
|
+
else {
|
|
81
|
+
process.stderr.write(`Unknown arg: ${a}\n`);
|
|
82
|
+
args.help = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --global is shorthand for --targets=claude-global
|
|
87
|
+
if (args.global) {
|
|
88
|
+
args.targets = ["claude-global"];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return args;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assert(condition, message) {
|
|
95
|
+
if (!condition) throw new Error(message);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isSymlink(p) {
|
|
99
|
+
try {
|
|
100
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function copyFileSyncPreserveMode(src, dest) {
|
|
107
|
+
const st = fs.statSync(src);
|
|
108
|
+
fs.copyFileSync(src, dest);
|
|
109
|
+
fs.chmodSync(dest, st.mode);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function copyDir({ srcDir, destDir }) {
|
|
113
|
+
if (isSymlink(srcDir)) throw new Error(`Refusing to copy symlink dir: ${srcDir}`);
|
|
114
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
117
|
+
for (const ent of entries) {
|
|
118
|
+
if (ent.name === ".DS_Store") continue;
|
|
119
|
+
const src = path.join(srcDir, ent.name);
|
|
120
|
+
const dest = path.join(destDir, ent.name);
|
|
121
|
+
|
|
122
|
+
if (isSymlink(src)) throw new Error(`Refusing to copy symlink: ${src}`);
|
|
123
|
+
|
|
124
|
+
if (ent.isDirectory()) {
|
|
125
|
+
copyDir({ srcDir: src, destDir: dest });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ent.isFile()) {
|
|
129
|
+
copyFileSyncPreserveMode(src, dest);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function listSkillDirs(skillsRoot) {
|
|
136
|
+
if (!fs.existsSync(skillsRoot)) return [];
|
|
137
|
+
return fs
|
|
138
|
+
.readdirSync(skillsRoot, { withFileTypes: true })
|
|
139
|
+
.filter((d) => d.isDirectory())
|
|
140
|
+
.map((d) => path.join(skillsRoot, d.name))
|
|
141
|
+
.filter((d) => fs.existsSync(path.join(d, "SKILL.md")));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const VALID_TARGETS = ["codex", "vscode", "claude", "claude-global", "cursor", "cursor-global", "antigravity"];
|
|
145
|
+
|
|
146
|
+
function detectSourceLayout(fromDir) {
|
|
147
|
+
const sourceSkillsDir = path.join(fromDir, "skills");
|
|
148
|
+
if (listSkillDirs(sourceSkillsDir).length > 0) {
|
|
149
|
+
return "source";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const distCandidates = [
|
|
153
|
+
path.join(fromDir, "codex", ".codex", "skills"),
|
|
154
|
+
path.join(fromDir, "vscode", ".github", "skills"),
|
|
155
|
+
path.join(fromDir, "claude", ".claude", "skills"),
|
|
156
|
+
path.join(fromDir, "cursor", ".cursor", "skills"),
|
|
157
|
+
path.join(fromDir, "antigravity", ".agent", "skill"),
|
|
158
|
+
];
|
|
159
|
+
if (distCandidates.some((p) => listSkillDirs(p).length > 0)) {
|
|
160
|
+
return "dist";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Could not detect source layout from: ${fromDir}. Expected either <from>/skills/* or dist-like target directories.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Map target to source directory
|
|
169
|
+
function getSourceDir(fromDir, target, sourceLayout) {
|
|
170
|
+
if (sourceLayout === "source") {
|
|
171
|
+
return path.join(fromDir, "skills");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// dist layout: claude-global uses same source as claude; cursor-global uses same as cursor
|
|
175
|
+
const sourceTarget =
|
|
176
|
+
target === "claude-global" ? "claude" : target === "cursor-global" ? "cursor" : target;
|
|
177
|
+
const targetDirMap = {
|
|
178
|
+
codex: path.join(fromDir, "codex", ".codex", "skills"),
|
|
179
|
+
vscode: path.join(fromDir, "vscode", ".github", "skills"),
|
|
180
|
+
claude: path.join(fromDir, "claude", ".claude", "skills"),
|
|
181
|
+
cursor: path.join(fromDir, "cursor", ".cursor", "skills"),
|
|
182
|
+
antigravity: path.join(fromDir, "antigravity", ".agent", "skill"),
|
|
183
|
+
};
|
|
184
|
+
return targetDirMap[sourceTarget];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Map target to destination directory
|
|
188
|
+
function getDestDir(destRepoRoot, target) {
|
|
189
|
+
// claude-global and cursor-global don't need destRepoRoot
|
|
190
|
+
if (target === "claude-global") {
|
|
191
|
+
return path.join(os.homedir(), ".claude", "skills");
|
|
192
|
+
}
|
|
193
|
+
if (target === "cursor-global") {
|
|
194
|
+
return path.join(os.homedir(), ".cursor", "skills");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Other targets require destRepoRoot
|
|
198
|
+
const destDirMap = {
|
|
199
|
+
codex: path.join(destRepoRoot, ".codex", "skills"),
|
|
200
|
+
vscode: path.join(destRepoRoot, ".github", "skills"),
|
|
201
|
+
claude: path.join(destRepoRoot, ".claude", "skills"),
|
|
202
|
+
cursor: path.join(destRepoRoot, ".cursor", "skills"),
|
|
203
|
+
antigravity: path.join(destRepoRoot, ".agent", "skill"),
|
|
204
|
+
};
|
|
205
|
+
return destDirMap[target];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function installTarget({ fromDir, destRepoRoot, target, skillsFilter, mode, dryRun, sourceLayout }) {
|
|
209
|
+
const srcSkillsRoot = getSourceDir(fromDir, target, sourceLayout);
|
|
210
|
+
const destSkillsRoot = getDestDir(destRepoRoot, target);
|
|
211
|
+
|
|
212
|
+
assert(srcSkillsRoot, `Unknown target: ${target}`);
|
|
213
|
+
assert(fs.existsSync(srcSkillsRoot), `Missing source skillpack dir: ${srcSkillsRoot}`);
|
|
214
|
+
|
|
215
|
+
let skillDirs = listSkillDirs(srcSkillsRoot);
|
|
216
|
+
assert(skillDirs.length > 0, `No skills found in: ${srcSkillsRoot}`);
|
|
217
|
+
|
|
218
|
+
// Filter skills if requested
|
|
219
|
+
if (skillsFilter.length > 0) {
|
|
220
|
+
const requested = new Set(skillsFilter);
|
|
221
|
+
const available = skillDirs.map((d) => path.basename(d));
|
|
222
|
+
|
|
223
|
+
// Validate requested skills exist
|
|
224
|
+
for (const s of requested) {
|
|
225
|
+
assert(available.includes(s), `Unknown skill: ${s}. Available: ${available.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
skillDirs = skillDirs.filter((d) => requested.has(path.basename(d)));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (dryRun) {
|
|
232
|
+
process.stdout.write(`[DRY-RUN] Would install ${skillDirs.length} skill(s) to ${destSkillsRoot}:\n`);
|
|
233
|
+
for (const d of skillDirs) {
|
|
234
|
+
process.stdout.write(` - ${path.basename(d)}\n`);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fs.mkdirSync(destSkillsRoot, { recursive: true });
|
|
240
|
+
|
|
241
|
+
for (const srcSkillDir of skillDirs) {
|
|
242
|
+
const name = path.basename(srcSkillDir);
|
|
243
|
+
const destSkillDir = path.join(destSkillsRoot, name);
|
|
244
|
+
|
|
245
|
+
if (mode === "replace") {
|
|
246
|
+
fs.rmSync(destSkillDir, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
copyDir({ srcDir: srcSkillDir, destDir: destSkillDir });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const isGlobal = target === "claude-global" || target === "cursor-global";
|
|
253
|
+
const location = isGlobal ? destSkillsRoot : path.relative(destRepoRoot, destSkillsRoot) || ".";
|
|
254
|
+
process.stdout.write(`OK: installed ${skillDirs.length} skill(s) to ${location}\n`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function listAvailableSkills(fromDir) {
|
|
258
|
+
const sourceLayout = detectSourceLayout(fromDir);
|
|
259
|
+
if (sourceLayout === "source") {
|
|
260
|
+
const skillDirs = listSkillDirs(path.join(fromDir, "skills"));
|
|
261
|
+
process.stdout.write("Available skills:\n");
|
|
262
|
+
for (const d of skillDirs) {
|
|
263
|
+
process.stdout.write(` - ${path.basename(d)}\n`);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// dist layout
|
|
269
|
+
const sources = ["codex", "vscode", "claude", "cursor", "antigravity"]
|
|
270
|
+
.map((t) => getSourceDir(fromDir, t, "dist"))
|
|
271
|
+
.filter((p) => fs.existsSync(p));
|
|
272
|
+
|
|
273
|
+
if (sources.length === 0) {
|
|
274
|
+
process.stderr.write("No built skills found. Run skillpack-build.mjs first.\n");
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const skillDirs = listSkillDirs(sources[0]);
|
|
279
|
+
process.stdout.write("Available skills:\n");
|
|
280
|
+
for (const d of skillDirs) {
|
|
281
|
+
process.stdout.write(` - ${path.basename(d)}\n`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function main() {
|
|
286
|
+
const args = parseArgs(process.argv.slice(2));
|
|
287
|
+
if (args.help) {
|
|
288
|
+
usage();
|
|
289
|
+
process.exit(2);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const repoRoot = process.cwd();
|
|
293
|
+
const fromDir = path.isAbsolute(args.from) ? args.from : path.join(repoRoot, args.from);
|
|
294
|
+
|
|
295
|
+
// Handle --list
|
|
296
|
+
if (args.list) {
|
|
297
|
+
listAvailableSkills(fromDir);
|
|
298
|
+
process.exit(0);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Validate targets
|
|
302
|
+
const targets = [...new Set(args.targets)];
|
|
303
|
+
for (const t of targets) {
|
|
304
|
+
assert(VALID_TARGETS.includes(t), `Invalid target: ${t}. Valid targets: ${VALID_TARGETS.join(", ")}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --dest is required unless only using global targets (claude-global, cursor-global)
|
|
308
|
+
const needsDest = targets.some((t) => t !== "claude-global" && t !== "cursor-global");
|
|
309
|
+
if (needsDest && !args.dest) {
|
|
310
|
+
process.stderr.write("Error: --dest is required for non-global targets.\n\n");
|
|
311
|
+
usage();
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const destRepoRoot = args.dest
|
|
316
|
+
? path.isAbsolute(args.dest)
|
|
317
|
+
? args.dest
|
|
318
|
+
: path.join(repoRoot, args.dest)
|
|
319
|
+
: null;
|
|
320
|
+
|
|
321
|
+
assert(args.mode === "replace" || args.mode === "merge", "mode must be 'replace' or 'merge'");
|
|
322
|
+
|
|
323
|
+
const sourceLayout = detectSourceLayout(fromDir);
|
|
324
|
+
|
|
325
|
+
for (const target of targets) {
|
|
326
|
+
installTarget({
|
|
327
|
+
fromDir,
|
|
328
|
+
destRepoRoot,
|
|
329
|
+
target,
|
|
330
|
+
skillsFilter: args.skills,
|
|
331
|
+
mode: args.mode,
|
|
332
|
+
dryRun: args.dryRun,
|
|
333
|
+
sourceLayout,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
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.
|