feat(agent-skills): add CLI wrapper scripts/skills/generate-agent-skills.mjs

Implements the CLI wrapper for the generator:
- --apply flag: enables write mode (default: dry-run)
- --prune flag: enables orphan detection/deletion
- --only=<id1,id2>: filters to specific skill IDs
- --json flag: structured JSON output
- Exit codes: 0 success, 1 error, 2 dry-run detected pending changes (CI)
- Human-readable table output with Generated/Unchanged/Pruned/Orphans/Errors summary
- Dynamic import via tsx/esm runtime; falls back to module.register() if needed
This commit is contained in:
diegosouzapw 2026-05-27 21:54:50 -03:00
parent f86e905efb
commit ca41880bb3

View file

@ -0,0 +1,183 @@
#!/usr/bin/env node
/**
* generate-agent-skills.mjs CLI wrapper for src/lib/agentSkills/generator.ts
*
* Usage:
* node scripts/skills/generate-agent-skills.mjs # dry-run (default)
* node scripts/skills/generate-agent-skills.mjs --apply # write SKILL.md files
* node scripts/skills/generate-agent-skills.mjs --prune # detect orphans (dry-run)
* node scripts/skills/generate-agent-skills.mjs --apply --prune # write + delete orphans
* node scripts/skills/generate-agent-skills.mjs --only=omni-providers,cli-serve
* node scripts/skills/generate-agent-skills.mjs --json # JSON output to stdout
*
* Exit codes:
* 0 success (dry-run with no changes, or apply completed)
* 1 error (import or generator threw)
* 2 dry-run detected changes (use for CI fail-on-stale check)
*
* Security: no shell interpolation of user input (Hard Rule #13).
* All runtime values are passed as JS variables, not shell strings.
*/
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import path from "node:path";
import process from "node:process";
// ── Resolve project root from script location ─────────────────────────────────
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// scripts/skills/generate-agent-skills.mjs → up 2 levels → project root
const projectRoot = path.resolve(__dirname, "..", "..");
// ── Argument parsing ──────────────────────────────────────────────────────────
const args = process.argv.slice(2);
function hasFlag(flag) {
return args.some((a) => a === flag || a.startsWith(flag + "="));
}
function getFlagValue(flag) {
for (const arg of args) {
if (arg.startsWith(flag + "=")) return arg.slice(flag.length + 1);
}
return null;
}
const applyMode = hasFlag("--apply");
const pruneMode = hasFlag("--prune");
const jsonOutput = hasFlag("--json");
const onlyRaw = getFlagValue("--only");
const onlyIds = onlyRaw ? onlyRaw.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
// ── Utility: print table ──────────────────────────────────────────────────────
function printTable(report) {
const { generated, unchanged, pruned, orphansDetected, errors } = report;
console.log(`\nGenerated: ${generated.length} · Unchanged: ${unchanged.length} · Pruned: ${pruned.length} · Orphans: ${orphansDetected.length} · Errors: ${errors.length}\n`);
if (generated.length > 0) {
console.log(" GENERATED:");
for (const id of generated) {
console.log(` + ${id}`);
}
}
if (unchanged.length > 0 && unchanged.length <= 10) {
console.log(" UNCHANGED:");
for (const id of unchanged) {
console.log(` = ${id}`);
}
} else if (unchanged.length > 10) {
console.log(` UNCHANGED: ${unchanged.length} skills (all up-to-date)`);
}
if (orphansDetected.length > 0) {
console.log(" ORPHANS DETECTED:");
for (const id of orphansDetected) {
console.log(` ? ${id}`);
}
}
if (pruned.length > 0) {
console.log(" PRUNED:");
for (const id of pruned) {
console.log(` - ${id}`);
}
}
if (errors.length > 0) {
console.log(" ERRORS:");
for (const e of errors) {
console.log(` ! ${e.id}: ${e.error}`);
}
}
}
// ── Main ─────────────────────────────────────────────────────────────────────
async function main() {
// Change CWD to project root so generator resolves files correctly
process.chdir(projectRoot);
if (!jsonOutput) {
const mode = applyMode ? "apply" : "dry-run";
const prune = pruneMode ? " + prune" : "";
const filter = onlyIds ? ` (only: ${onlyIds.join(", ")})` : "";
console.log(`\nAgent Skills Generator [${mode}${prune}]${filter}`);
console.log("─".repeat(60));
}
// Dynamic import via tsx runtime — works because package.json has tsx devDep
// and node is invoked with --import tsx/esm by the caller (or we use tsx directly).
// To support plain `node` invocation, we use a dynamic import with tsx register.
let generateAgentSkills;
try {
// Try direct import first (when running under tsx or compiled)
const mod = await import("../../src/lib/agentSkills/generator.ts");
generateAgentSkills = mod.generateAgentSkills;
} catch (importErr) {
// Fallback: try tsx register approach
try {
const require = createRequire(import.meta.url);
// Register tsx for TypeScript support
const tsxPath = require.resolve("tsx/esm");
const { register } = await import("node:module");
register(tsxPath, import.meta.url);
const mod = await import("../../src/lib/agentSkills/generator.ts");
generateAgentSkills = mod.generateAgentSkills;
} catch (fallbackErr) {
console.error(
"Error: Could not import generator. Run with tsx:\n" +
" node --import tsx/esm scripts/skills/generate-agent-skills.mjs\n" +
`Import error: ${importErr instanceof Error ? importErr.message : String(importErr)}`,
);
process.exit(1);
}
}
let report;
try {
report = await generateAgentSkills({
dryRun: !applyMode,
prune: pruneMode,
outputDir: "skills",
onlyIds,
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (jsonOutput) {
console.log(JSON.stringify({ error: msg }, null, 2));
} else {
console.error(`\nGenerator error: ${msg}`);
}
process.exit(1);
}
if (jsonOutput) {
console.log(JSON.stringify(report, null, 2));
} else {
printTable(report);
if (!applyMode) {
console.log(
"\n(dry-run) No files written. Use --apply to write SKILL.md files.\n",
);
} else {
console.log();
}
}
// Exit code 2 if dry-run detected pending changes (useful for CI)
if (!applyMode && report.generated.length > 0) {
process.exit(2);
}
process.exit(0);
}
main().catch((err) => {
console.error("Unexpected error:", err instanceof Error ? err.message : String(err));
process.exit(1);
});