diff --git a/docs/packaging.md b/docs/packaging.md index 9a629af..fa15893 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -16,13 +16,42 @@ Outputs: - `dist/vscode/.github/skills/*` (VS Code / Copilot repo layout) - `dist/claude/.claude/skills/*` (Claude Code repo layout) - `dist/cursor/.cursor/skills/*` (Cursor repo layout) +- `dist/gemini/.gemini/*` (Gemini CLI extension layout) ## Install into another repo 1. Build dist (above). 2. Install into a destination repo: -- `node shared/scripts/skillpack-install.mjs --dest=../some-repo --targets=codex,vscode,claude,cursor` +- `node shared/scripts/skillpack-install.mjs --dest=../some-repo --targets=codex,vscode,claude,cursor,gemini` By default, install mode is `replace` (it replaces only the skill directories it installs). +## Gemini CLI Extension + +To use these skills with [Gemini CLI](https://github.com/google/gemini-cli), the build process generates a modular extension with individual skills located in the `skills/` directory. This allows Gemini CLI to load only the necessary skills on demand, optimizing context usage. + +### Build and Install + +1. **Build the package**: + ```bash + node shared/scripts/skillpack-build.mjs --targets=gemini + ``` + +2. **Install via Gemini CLI (Recommended)**: + The native CLI command is the safest method. It handles security trust prompts, registers the extension properly, and enables on-demand skill activation. + ```bash + gemini extension install ./dist/gemini/.gemini + ``` + +3. **Install via script (Alternative)**: + For headless environments or automated setups, you can install the extension globally to `~/.gemini/extensions/wordpress-agent-skills/`: + ```bash + node shared/scripts/skillpack-install.mjs --targets=gemini-global + ``` + +### Managing the Extension +To update the extension, you must uninstall it first. Always use the registered extension name (found in `gemini-extension.json`), not the file path: +```bash +gemini extension uninstall wordpress-agent-skills +``` diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 0000000..eed0524 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,6 @@ +{ + "name": "wordpress-agent-skills", + "version": "1.0.0", + "description": "Official WordPress Core engineering standards and agent skills", + "contextFileName": "GEMINI.md" +} diff --git a/shared/scripts/skillpack-build.mjs b/shared/scripts/skillpack-build.mjs index a5841de..51a433b 100644 --- a/shared/scripts/skillpack-build.mjs +++ b/shared/scripts/skillpack-build.mjs @@ -5,16 +5,17 @@ function usage() { process.stderr.write( [ "Usage:", - " node shared/scripts/skillpack-build.mjs [--out=dist] [--targets=codex,vscode,claude,cursor] [--skills=skill1,skill2] [--clean]", + " node shared/scripts/skillpack-build.mjs [--out=dist] [--targets=codex,vscode,claude,cursor,gemini] [--skills=skill1,skill2] [--clean]", "", "Outputs:", " - /codex/.codex/skills//SKILL.md", " - /vscode/.github/skills//SKILL.md", " - /claude/.claude/skills//SKILL.md", " - /cursor/.cursor/skills//SKILL.md", + " - /gemini/.gemini/GEMINI.md", "", "Options:", - " --targets Comma-separated list of targets (codex, vscode, claude, cursor). Default: codex,vscode,claude,cursor", + " --targets Comma-separated list of targets (codex, vscode, claude, cursor, gemini). Default: codex,vscode,claude,cursor,gemini", " --skills Comma-separated list of skill names to build. Default: all skills", " --clean Remove target directories before building", "", @@ -26,7 +27,7 @@ function usage() { } function parseArgs(argv) { - const args = { out: "dist", targets: ["codex", "vscode", "claude", "cursor"], skills: [], clean: false }; + const args = { out: "dist", targets: ["codex", "vscode", "claude", "cursor", "gemini"], skills: [], clean: false }; for (const a of argv) { if (a === "--help" || a === "-h") args.help = true; else if (a === "--clean") args.clean = true; @@ -101,23 +102,109 @@ function buildTarget({ repoRoot, outDir, target, skillDirs }) { vscode: path.join(outDir, "vscode", ".github", "skills"), claude: path.join(outDir, "claude", ".claude", "skills"), cursor: path.join(outDir, "cursor", ".cursor", "skills"), + gemini: path.join(outDir, "gemini", ".gemini"), }; const destSkillsRoot = rootByTarget[target]; assert(destSkillsRoot, `Unknown target: ${target}`); fs.mkdirSync(destSkillsRoot, { recursive: true }); - for (const srcSkillDir of skillDirs) { - const name = path.basename(srcSkillDir); - const destSkillDir = path.join(destSkillsRoot, name); - copyDir({ srcDir: srcSkillDir, destDir: destSkillDir }); + + if (target === "gemini") { + // 1. CREATE ROOT GEMINI.md + let rootContent = "# WordPress Core Standards & Agent Skills\n\n"; + rootContent += "## 🛠️ Script Execution Paths\n"; + rootContent += "Executable scripts for these skills are located in either:\n"; + rootContent += "- `./.gemini/skills/` (if installed locally in a project)\n"; + rootContent += "- `~/.gemini/extensions/wordpress-agent-skills/skills/` (if installed globally)\n\n"; + rootContent += "Please use these paths when instructed to run `node` commands.\n"; + fs.writeFileSync(path.join(destSkillsRoot, "GEMINI.md"), rootContent); + + // 2. COPY COMMANDS + const commandsSrcDir = path.join(repoRoot, "commands"); + if (fs.existsSync(commandsSrcDir)) { + const commandsDestDir = path.join(destSkillsRoot, "commands"); + fs.mkdirSync(commandsDestDir, { recursive: true }); + const commandFiles = fs.readdirSync(commandsSrcDir).filter((f) => f.endsWith(".md")); + for (const cmdFile of commandFiles) { + fs.copyFileSync(path.join(commandsSrcDir, cmdFile), path.join(commandsDestDir, cmdFile)); + } + } + + // 3. PROCESS INDIVIDUAL SKILLS + const destSkillsDir = path.join(destSkillsRoot, "skills"); + fs.mkdirSync(destSkillsDir, { recursive: true }); + + for (const srcSkillDir of skillDirs) { + const skillName = path.basename(srcSkillDir); + const destSkillDir = path.join(destSkillsDir, skillName); + fs.mkdirSync(destSkillDir, { recursive: true }); + + // Build the SKILL.md for this specific skill + const srcSkillFile = path.join(srcSkillDir, "SKILL.md"); + if (fs.existsSync(srcSkillFile)) { + let content = fs.readFileSync(srcSkillFile, "utf8"); + + // Extract frontmatter to keep it at the top + let frontmatter = ""; + const frontmatterMatch = content.match(/^---\n[\s\S]*?\n---\n*/); + if (frontmatterMatch) { + frontmatter = frontmatterMatch[0]; + content = content.replace(/^---\n[\s\S]*?\n---\n*/, ""); + } + + // Add references if they exist + const refsDir = path.join(srcSkillDir, "references"); + if (fs.existsSync(refsDir)) { + const refs = fs.readdirSync(refsDir).filter((f) => f.endsWith(".md") || f.endsWith(".json")).sort(); + for (const ref of refs) { + const refPath = path.join(refsDir, ref); + let refContent = fs.readFileSync(refPath, "utf8"); + const relPath = path.relative(repoRoot, refPath); + + if (ref.endsWith(".json")) { + refContent = `\n\`\`\`json\n${refContent}\n\`\`\`\n`; + } + + content += `\n\n***\n### 📄 Source: ${relPath}\n\n${refContent}`; + } + } + + // Apply transformations + content = content.replace(/node skills\//g, 'node .gemini/skills/'); + content = content.replace(/@([a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+)/g, '@$1'); + + // Write the combined SKILL.md + fs.writeFileSync(path.join(destSkillDir, "SKILL.md"), frontmatter + content); + } + + // Copy scripts for this skill + const scriptsSrc = path.join(srcSkillDir, "scripts"); + if (fs.existsSync(scriptsSrc)) { + const scriptsDest = path.join(destSkillDir, "scripts"); + copyDir({ srcDir: scriptsSrc, destDir: scriptsDest }); + } + } + + // 4. COPY MANIFEST + const manifestSrc = path.join(repoRoot, "gemini-extension.json"); + if (fs.existsSync(manifestSrc)) { + fs.copyFileSync(manifestSrc, path.join(destSkillsRoot, "gemini-extension.json")); + } + } else { + // Standard logic for Codex, VS Code, Claude, and Cursor + for (const srcSkillDir of skillDirs) { + const name = path.basename(srcSkillDir); + const destSkillDir = path.join(destSkillsRoot, name); + copyDir({ srcDir: srcSkillDir, destDir: destSkillDir }); + } } const rel = path.relative(repoRoot, destSkillsRoot); process.stdout.write(`OK: built ${target} skillpack at ${rel}\n`); } -const VALID_TARGETS = ["codex", "vscode", "claude", "cursor"]; +const VALID_TARGETS = ["codex", "vscode", "claude", "cursor", "gemini"]; function main() { const args = parseArgs(process.argv.slice(2)); @@ -164,4 +251,3 @@ function main() { } main(); - diff --git a/shared/scripts/skillpack-install.mjs b/shared/scripts/skillpack-install.mjs index e9411aa..06ad153 100644 --- a/shared/scripts/skillpack-install.mjs +++ b/shared/scripts/skillpack-install.mjs @@ -11,7 +11,7 @@ function usage() { "Options:", " --dest= Destination repo root (required, unless using --global)", " --from= Source directory (default: dist)", - " --targets= Comma-separated targets: codex, vscode, claude, claude-global, cursor, cursor-global (default: codex,vscode)", + " --targets= Comma-separated targets: codex, vscode, claude, claude-global, cursor, cursor-global, gemini, gemini-global (default: codex,vscode)", " --skills= Comma-separated skill names to install (default: all)", " --mode= 'replace' (default) or 'merge'", " --global Shorthand for --targets=claude-global (installs to ~/.claude/skills)", @@ -25,11 +25,13 @@ function usage() { " claude-global Install to ~/.claude/skills/ (user-level, ignores --dest)", " cursor Install to /.cursor/skills/", " cursor-global Install to ~/.cursor/skills/ (user-level, ignores --dest)", + " gemini Install to /.gemini/", + " gemini-global Install to ~/.gemini/ (user-level, ignores --dest)", "", "Examples:", " # Build and install to a WordPress project", " node shared/scripts/skillpack-build.mjs --clean", - " node shared/scripts/skillpack-install.mjs --dest=../my-wp-repo --targets=codex,vscode,claude,cursor", + " node shared/scripts/skillpack-install.mjs --dest=../my-wp-repo --targets=codex,vscode,claude,cursor,gemini", "", " # Install globally for Claude Code (all skills)", " node shared/scripts/skillpack-install.mjs --global", @@ -37,11 +39,14 @@ function usage() { " # Install globally for Cursor (all skills)", " node shared/scripts/skillpack-install.mjs --targets=cursor-global", "", + " # Install globally for Gemini CLI (all skills)", + " node shared/scripts/skillpack-install.mjs --targets=gemini-global", + "", " # Install specific skills globally", " node shared/scripts/skillpack-install.mjs --global --skills=wp-playground,wp-block-development", "", " # Install to project with specific skills", - " node shared/scripts/skillpack-install.mjs --dest=../my-repo --targets=claude,cursor --skills=wp-wpcli-and-ops", + " node shared/scripts/skillpack-install.mjs --dest=../my-repo --targets=claude,cursor,gemini --skills=wp-wpcli-and-ops", "", ].join("\n") ); @@ -126,6 +131,22 @@ function copyDir({ srcDir, destDir }) { function listSkillDirs(skillsRoot) { if (!fs.existsSync(skillsRoot)) return []; + + // For Gemini extensions, the skills are usually in a 'skills' subdirectory + const geminiSkillsDir = path.join(skillsRoot, "skills"); + if (fs.existsSync(geminiSkillsDir) && fs.statSync(geminiSkillsDir).isDirectory()) { + return fs + .readdirSync(geminiSkillsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => path.join(geminiSkillsDir, d.name)) + .filter((d) => fs.existsSync(path.join(d, "SKILL.md"))); + } + + // Legacy/Aggregated Gemini check + if (fs.existsSync(path.join(skillsRoot, "GEMINI.md")) && !fs.existsSync(geminiSkillsDir)) { + return [skillsRoot]; + } + return fs .readdirSync(skillsRoot, { withFileTypes: true }) .filter((d) => d.isDirectory()) @@ -133,31 +154,28 @@ function listSkillDirs(skillsRoot) { .filter((d) => fs.existsSync(path.join(d, "SKILL.md"))); } -const VALID_TARGETS = ["codex", "vscode", "claude", "claude-global", "cursor", "cursor-global"]; +const VALID_TARGETS = ["codex", "vscode", "claude", "claude-global", "cursor", "cursor-global", "gemini", "gemini-global"]; // Map target to source subdirectory in dist function getSourceDir(fromDir, target) { - // claude-global uses the same source as claude; cursor-global uses the same as cursor - const sourceTarget = - target === "claude-global" ? "claude" : target === "cursor-global" ? "cursor" : target; + // Map global targets to their respective sources + const sourceTarget = target.endsWith("-global") ? target.replace("-global", "") : target; const targetDirMap = { codex: path.join(fromDir, "codex", ".codex", "skills"), vscode: path.join(fromDir, "vscode", ".github", "skills"), claude: path.join(fromDir, "claude", ".claude", "skills"), cursor: path.join(fromDir, "cursor", ".cursor", "skills"), + gemini: path.join(fromDir, "gemini", ".gemini"), }; return targetDirMap[sourceTarget]; } // Map target to destination directory function getDestDir(destRepoRoot, target) { - // claude-global and cursor-global don't need destRepoRoot - if (target === "claude-global") { - return path.join(os.homedir(), ".claude", "skills"); - } - if (target === "cursor-global") { - return path.join(os.homedir(), ".cursor", "skills"); - } + // Global targets don't need destRepoRoot + if (target === "claude-global") return path.join(os.homedir(), ".claude", "skills"); + if (target === "cursor-global") return path.join(os.homedir(), ".cursor", "skills"); + if (target === "gemini-global") return path.join(os.homedir(), ".gemini", "extensions", "wordpress-agent-skills"); // Other targets require destRepoRoot const destDirMap = { @@ -165,6 +183,7 @@ function getDestDir(destRepoRoot, target) { vscode: path.join(destRepoRoot, ".github", "skills"), claude: path.join(destRepoRoot, ".claude", "skills"), cursor: path.join(destRepoRoot, ".cursor", "skills"), + gemini: path.join(destRepoRoot, ".gemini"), }; return destDirMap[target]; } @@ -179,8 +198,8 @@ function installTarget({ fromDir, destRepoRoot, target, skillsFilter, mode, dryR let skillDirs = listSkillDirs(srcSkillsRoot); assert(skillDirs.length > 0, `No skills found in: ${srcSkillsRoot}`); - // Filter skills if requested - if (skillsFilter.length > 0) { + // Filter skills if requested (only applies to non-aggregated targets) + if (skillsFilter.length > 0 && !target.includes("gemini")) { const requested = new Set(skillsFilter); const available = skillDirs.map((d) => path.basename(d)); @@ -202,25 +221,30 @@ function installTarget({ fromDir, destRepoRoot, target, skillsFilter, mode, dryR fs.mkdirSync(destSkillsRoot, { recursive: true }); - for (const srcSkillDir of skillDirs) { - const name = path.basename(srcSkillDir); - const destSkillDir = path.join(destSkillsRoot, name); + if (target.includes("gemini")) { + // For Gemini, we copy the entire .gemini source directory contents + copyDir({ srcDir: srcSkillsRoot, destDir: destSkillsRoot }); + } else { + for (const srcSkillDir of skillDirs) { + const name = path.basename(srcSkillDir); + const destSkillDir = path.join(destSkillsRoot, name); - if (mode === "replace") { - fs.rmSync(destSkillDir, { recursive: true, force: true }); - } + if (mode === "replace") { + fs.rmSync(destSkillDir, { recursive: true, force: true }); + } - copyDir({ srcDir: srcSkillDir, destDir: destSkillDir }); + copyDir({ srcDir: srcSkillDir, destDir: destSkillDir }); + } } - const isGlobal = target === "claude-global" || target === "cursor-global"; + const isGlobal = target.endsWith("-global"); const location = isGlobal ? destSkillsRoot : path.relative(destRepoRoot, destSkillsRoot) || "."; process.stdout.write(`OK: installed ${skillDirs.length} skill(s) to ${location}\n`); } function listAvailableSkills(fromDir) { // Check all possible target sources - const sources = ["codex", "vscode", "claude", "cursor"] + const sources = ["codex", "vscode", "claude", "cursor", "gemini"] .map((t) => getSourceDir(fromDir, t)) .filter((p) => fs.existsSync(p)); @@ -258,8 +282,8 @@ function main() { assert(VALID_TARGETS.includes(t), `Invalid target: ${t}. Valid targets: ${VALID_TARGETS.join(", ")}`); } - // --dest is required unless only using global targets (claude-global, cursor-global) - const needsDest = targets.some((t) => t !== "claude-global" && t !== "cursor-global"); + // --dest is required unless only using global targets + const needsDest = targets.some((t) => !t.endsWith("-global")); if (needsDest && !args.dest) { process.stderr.write("Error: --dest is required for non-global targets.\n\n"); usage();