Files
Fellyph Cintra 8182820cab Add Antigravity target to skillpack build and install scripts (#13)
This pull request adds opt-in Antigravity support to the skillpack build
and install scripts.

Documentation updates for skill file locations:

* Adds `.agents/skills/` as the project-level directory for Antigravity
skill files.
* Adds `antigravity-global` for installing skills to
`~/.gemini/antigravity/skills/`.
* Keeps Antigravity out of the default build and install target lists so
existing defaults remain unchanged.

🤖 Partially developed with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 09:43:09 +02:00

308 lines
10 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import os from "node:os";
function usage() {
process.stderr.write(
[
"Usage:",
" node shared/scripts/skillpack-install.mjs --dest=<repo-root> [options]",
"",
"Options:",
" --dest=<path> Destination repo root (required, unless using --global)",
" --from=<path> Source directory (default: dist)",
" --targets=<list> Comma-separated targets: codex, vscode, claude, claude-global, cursor, cursor-global, antigravity, antigravity-global (default: codex,vscode)",
" --skills=<list> Comma-separated skill names to install (default: all)",
" --mode=<mode> 'replace' (default) or 'merge'",
" --global Shorthand for --targets=claude-global (installs to ~/.claude/skills)",
" --dry-run Show what would be installed without making changes",
" --list List available skills and exit",
"",
"Targets:",
" codex Install to <dest>/.codex/skills/",
" vscode Install to <dest>/.github/skills/",
" claude Install to <dest>/.claude/skills/ (project-level)",
" claude-global Install to ~/.claude/skills/ (user-level, ignores --dest)",
" cursor Install to <dest>/.cursor/skills/",
" cursor-global Install to ~/.cursor/skills/ (user-level, ignores --dest)",
" antigravity Install to <dest>/.agents/skills/",
" antigravity-global Install to ~/.gemini/antigravity/skills/ (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",
"",
" # Install globally for Claude Code (all skills)",
" node shared/scripts/skillpack-install.mjs --global",
"",
" # Install globally for Cursor (all skills)",
" node shared/scripts/skillpack-install.mjs --targets=cursor-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",
"",
].join("\n")
);
}
function parseArgs(argv) {
const args = {
from: "dist",
dest: null,
targets: ["codex", "vscode"],
skills: [],
mode: "replace",
dryRun: false,
global: false,
list: false,
};
for (const a of argv) {
if (a === "--help" || a === "-h") args.help = true;
else if (a === "--dry-run") args.dryRun = true;
else if (a === "--global") args.global = true;
else if (a === "--list") args.list = true;
else if (a.startsWith("--from=")) args.from = a.slice("--from=".length);
else if (a.startsWith("--dest=")) args.dest = a.slice("--dest=".length);
else if (a.startsWith("--targets=")) args.targets = a.slice("--targets=".length).split(",").filter(Boolean);
else if (a.startsWith("--skills=")) args.skills = a.slice("--skills=".length).split(",").filter(Boolean);
else if (a.startsWith("--mode=")) args.mode = a.slice("--mode=".length);
else {
process.stderr.write(`Unknown arg: ${a}\n`);
args.help = true;
}
}
// --global is shorthand for --targets=claude-global
if (args.global) {
args.targets = ["claude-global"];
}
return args;
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function isSymlink(p) {
try {
return fs.lstatSync(p).isSymbolicLink();
} catch {
return false;
}
}
function copyFileSyncPreserveMode(src, dest) {
const st = fs.statSync(src);
fs.copyFileSync(src, dest);
fs.chmodSync(dest, st.mode);
}
function copyDir({ srcDir, destDir }) {
if (isSymlink(srcDir)) throw new Error(`Refusing to copy symlink dir: ${srcDir}`);
fs.mkdirSync(destDir, { recursive: true });
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
for (const ent of entries) {
if (ent.name === ".DS_Store") continue;
const src = path.join(srcDir, ent.name);
const dest = path.join(destDir, ent.name);
if (isSymlink(src)) throw new Error(`Refusing to copy symlink: ${src}`);
if (ent.isDirectory()) {
copyDir({ srcDir: src, destDir: dest });
continue;
}
if (ent.isFile()) {
copyFileSyncPreserveMode(src, dest);
continue;
}
}
}
function listSkillDirs(skillsRoot) {
if (!fs.existsSync(skillsRoot)) return [];
return fs
.readdirSync(skillsRoot, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => path.join(skillsRoot, d.name))
.filter((d) => fs.existsSync(path.join(d, "SKILL.md")));
}
const VALID_TARGETS = ["codex", "vscode", "claude", "claude-global", "cursor", "cursor-global", "antigravity", "antigravity-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;
// antigravity-global uses the same source as antigravity
const sourceTarget =
target === "claude-global"
? "claude"
: target === "cursor-global"
? "cursor"
: target === "antigravity-global"
? "antigravity"
: 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"),
antigravity: path.join(fromDir, "antigravity", ".agents", "skills"),
};
return targetDirMap[sourceTarget];
}
// Map target to destination directory
function getDestDir(destRepoRoot, target) {
// 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 === "antigravity-global") {
return path.join(os.homedir(), ".gemini", "antigravity", "skills");
}
// Other targets require destRepoRoot
const destDirMap = {
codex: path.join(destRepoRoot, ".codex", "skills"),
vscode: path.join(destRepoRoot, ".github", "skills"),
claude: path.join(destRepoRoot, ".claude", "skills"),
cursor: path.join(destRepoRoot, ".cursor", "skills"),
antigravity: path.join(destRepoRoot, ".agents", "skills"),
};
return destDirMap[target];
}
function installTarget({ fromDir, destRepoRoot, target, skillsFilter, mode, dryRun }) {
const srcSkillsRoot = getSourceDir(fromDir, target);
const destSkillsRoot = getDestDir(destRepoRoot, target);
assert(srcSkillsRoot, `Unknown target: ${target}`);
assert(fs.existsSync(srcSkillsRoot), `Missing source skillpack dir: ${srcSkillsRoot}. Did you run skillpack-build.mjs first?`);
let skillDirs = listSkillDirs(srcSkillsRoot);
assert(skillDirs.length > 0, `No skills found in: ${srcSkillsRoot}`);
// Filter skills if requested
if (skillsFilter.length > 0) {
const requested = new Set(skillsFilter);
const available = skillDirs.map((d) => path.basename(d));
// Validate requested skills exist
for (const s of requested) {
assert(available.includes(s), `Unknown skill: ${s}. Available: ${available.join(", ")}`);
}
skillDirs = skillDirs.filter((d) => requested.has(path.basename(d)));
}
if (dryRun) {
process.stdout.write(`[DRY-RUN] Would install ${skillDirs.length} skill(s) to ${destSkillsRoot}:\n`);
for (const d of skillDirs) {
process.stdout.write(` - ${path.basename(d)}\n`);
}
return;
}
fs.mkdirSync(destSkillsRoot, { recursive: true });
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 });
}
copyDir({ srcDir: srcSkillDir, destDir: destSkillDir });
}
const isGlobal =
target === "claude-global" ||
target === "cursor-global" ||
target === "antigravity-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", "antigravity"]
.map((t) => getSourceDir(fromDir, t))
.filter((p) => fs.existsSync(p));
if (sources.length === 0) {
process.stderr.write("No built skills found. Run skillpack-build.mjs first.\n");
process.exit(1);
}
const skillDirs = listSkillDirs(sources[0]);
process.stdout.write("Available skills:\n");
for (const d of skillDirs) {
process.stdout.write(` - ${path.basename(d)}\n`);
}
}
function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
process.exit(2);
}
const repoRoot = process.cwd();
const fromDir = path.isAbsolute(args.from) ? args.from : path.join(repoRoot, args.from);
// Handle --list
if (args.list) {
listAvailableSkills(fromDir);
process.exit(0);
}
// Validate targets
const targets = [...new Set(args.targets)];
for (const t of targets) {
assert(VALID_TARGETS.includes(t), `Invalid target: ${t}. Valid targets: ${VALID_TARGETS.join(", ")}`);
}
// --dest is required unless only using global targets
const globalTargets = new Set(["claude-global", "cursor-global", "antigravity-global"]);
const needsDest = targets.some((t) => !globalTargets.has(t));
if (needsDest && !args.dest) {
process.stderr.write("Error: --dest is required for non-global targets.\n\n");
usage();
process.exit(1);
}
const destRepoRoot = args.dest
? path.isAbsolute(args.dest)
? args.dest
: path.join(repoRoot, args.dest)
: null;
assert(args.mode === "replace" || args.mode === "merge", "mode must be 'replace' or 'merge'");
for (const target of targets) {
installTarget({
fromDir,
destRepoRoot,
target,
skillsFilter: args.skills,
mode: args.mode,
dryRun: args.dryRun,
});
}
}
main();