mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-09 17:28:51 +00:00
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:
parent
f86e905efb
commit
ca41880bb3
1 changed files with 183 additions and 0 deletions
183
scripts/skills/generate-agent-skills.mjs
Normal file
183
scripts/skills/generate-agent-skills.mjs
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue