feat(M3): Shared-source generator for agent variants
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* generate-skills.mjs — shared-source generator for agent variants (M3, S-302)
|
||||
*
|
||||
* Generates every agent-variant directory (`skills/<skill>/<agent>/`) and
|
||||
* `pi-package/skills/<skill>/` mirror from canonical sources. Generated files
|
||||
* carry file-type-aware headers; each generated root gets a non-self-referential
|
||||
* `.generated-manifest.json`.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/generate-skills.mjs # regenerate everything
|
||||
* pnpm run sync:pi # same via pnpm alias
|
||||
*
|
||||
* Exported helpers (used by verify-generated.mjs and tests):
|
||||
* detectFileType(filePath) → string
|
||||
* applyHeader(content, fileType, canonicalHint) → string
|
||||
* makePackageJsonContent(sourcePkg, skillName, agentName) → object
|
||||
* getGeneratedRoots(repoRoot?) → string[]
|
||||
* buildManifest(generatedRootAbs, generatedRootRel) → Promise<object>
|
||||
* generateSkills(repoRoot, options?) → Promise<{generatedRoots: string[]}>
|
||||
*/
|
||||
|
||||
import {
|
||||
lstat,
|
||||
mkdir,
|
||||
readdir,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
||||
|
||||
// ── Constants ──────────────────────────────────────────────────────────────
|
||||
|
||||
const MANIFEST_SCHEMA =
|
||||
"https://ai-coding-skills.dev/schemas/generated-manifest/v1.json";
|
||||
const MANIFEST_GENERATOR = "scripts/generate-skills.mjs";
|
||||
const MANIFEST_FILENAME = ".generated-manifest.json";
|
||||
|
||||
const AGENTS = ["claude-code", "codex", "cursor", "opencode", "pi"];
|
||||
|
||||
/**
|
||||
* Canonical list of all generated roots, relative to repo root.
|
||||
* Verifier uses this to know which directories to walk.
|
||||
*/
|
||||
const GENERATED_ROOTS = [
|
||||
// atlassian agent variants
|
||||
"skills/atlassian/claude-code",
|
||||
"skills/atlassian/codex",
|
||||
"skills/atlassian/cursor",
|
||||
"skills/atlassian/opencode",
|
||||
"skills/atlassian/pi",
|
||||
// web-automation agent variants
|
||||
"skills/web-automation/claude-code",
|
||||
"skills/web-automation/codex",
|
||||
"skills/web-automation/cursor",
|
||||
"skills/web-automation/opencode",
|
||||
"skills/web-automation/pi",
|
||||
// create-plan agent variants
|
||||
"skills/create-plan/claude-code",
|
||||
"skills/create-plan/codex",
|
||||
"skills/create-plan/cursor",
|
||||
"skills/create-plan/opencode",
|
||||
"skills/create-plan/pi",
|
||||
// do-task agent variants
|
||||
"skills/do-task/claude-code",
|
||||
"skills/do-task/codex",
|
||||
"skills/do-task/cursor",
|
||||
"skills/do-task/opencode",
|
||||
"skills/do-task/pi",
|
||||
// implement-plan agent variants
|
||||
"skills/implement-plan/claude-code",
|
||||
"skills/implement-plan/codex",
|
||||
"skills/implement-plan/cursor",
|
||||
"skills/implement-plan/opencode",
|
||||
"skills/implement-plan/pi",
|
||||
// reviewer-runtime pi variant
|
||||
"skills/reviewer-runtime/pi",
|
||||
// pi-package mirrors
|
||||
"pi-package/skills/atlassian",
|
||||
"pi-package/skills/create-plan",
|
||||
"pi-package/skills/do-task",
|
||||
"pi-package/skills/implement-plan",
|
||||
"pi-package/skills/web-automation",
|
||||
];
|
||||
|
||||
// ── File-type detection ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Classify a file path into a header policy category.
|
||||
* @param {string} filePath - Relative or absolute path to the file.
|
||||
* @returns {'markdown'|'shell'|'ts'|'js'|'json'|'yaml'|'jsonc'|'unknown'}
|
||||
*/
|
||||
export function detectFileType(filePath) {
|
||||
const base = path.basename(filePath);
|
||||
const ext = path.extname(base).toLowerCase();
|
||||
|
||||
if (ext === ".md") return "markdown";
|
||||
if (ext === ".sh") return "shell";
|
||||
if (ext === ".ts") return "ts";
|
||||
if (ext === ".js") return "js";
|
||||
if (ext === ".jsonc") return "jsonc";
|
||||
if (ext === ".json") return "json";
|
||||
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// ── Header insertion ──────────────────────────────────────────────────────
|
||||
|
||||
const HEADER_MSG = (hint) =>
|
||||
`⚠️ GENERATED FILE – do not edit directly. Edit the canonical source in ${hint} and run \`pnpm run sync:pi\`.`;
|
||||
|
||||
/**
|
||||
* Insert a file-type-aware generated-file header into content.
|
||||
*
|
||||
* Policy:
|
||||
* markdown → HTML comment after YAML front matter (or at top if no front matter)
|
||||
* shell → # comment after shebang (never before it)
|
||||
* ts/js → // comment at top
|
||||
* jsonc → // comment at top
|
||||
* yaml → # comment at top (pnpm-lock.yaml skipped by caller)
|
||||
* json → no header (recorded in .generated-manifest.json)
|
||||
* unknown → no header
|
||||
*
|
||||
* @param {string} content - Original file content.
|
||||
* @param {string} fileType - Output of detectFileType().
|
||||
* @param {string} canonicalHint - Human-readable hint to canonical source location.
|
||||
* @returns {string} Content with header inserted, or original if no header applies.
|
||||
*/
|
||||
export function applyHeader(content, fileType, canonicalHint) {
|
||||
const msg = HEADER_MSG(canonicalHint);
|
||||
|
||||
switch (fileType) {
|
||||
case "markdown": {
|
||||
// Insert HTML comment after YAML front matter closing ---, or at top
|
||||
if (content.startsWith("---\n")) {
|
||||
const closingIdx = content.indexOf("\n---\n", 4);
|
||||
if (closingIdx !== -1) {
|
||||
const after = closingIdx + 5; // length of "\n---\n"
|
||||
const before = content.slice(0, after);
|
||||
const rest = content.slice(after);
|
||||
// Ensure the comment is on its own line, with a blank line before/after
|
||||
return `${before}\n<!-- ${msg} -->\n${rest}`;
|
||||
}
|
||||
}
|
||||
return `<!-- ${msg} -->\n${content}`;
|
||||
}
|
||||
|
||||
case "shell": {
|
||||
// Insert # comment AFTER shebang line (never before it)
|
||||
const lines = content.split("\n");
|
||||
if (lines[0].startsWith("#!")) {
|
||||
return [lines[0], `# ${msg}`, ...lines.slice(1)].join("\n");
|
||||
}
|
||||
return `# ${msg}\n${content}`;
|
||||
}
|
||||
|
||||
case "ts":
|
||||
case "js":
|
||||
case "jsonc": {
|
||||
// Insert after shebang if present (TypeScript requires #! on line 1)
|
||||
const lines = content.split("\n");
|
||||
if (lines[0].startsWith("#!")) {
|
||||
return [lines[0], `// ${msg}`, ...lines.slice(1)].join("\n");
|
||||
}
|
||||
return `// ${msg}\n${content}`;
|
||||
}
|
||||
|
||||
case "yaml": {
|
||||
return `# ${msg}\n${content}`;
|
||||
}
|
||||
|
||||
case "json":
|
||||
case "unknown":
|
||||
default:
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
// ── package.json transformation ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Produce a modified package.json object with a unique scoped name and
|
||||
* `"private": true` for an agent-variant generated root.
|
||||
*
|
||||
* @param {object} sourcePkg - Parsed source package.json object.
|
||||
* @param {string} skillName - Skill identifier (e.g. "atlassian").
|
||||
* @param {string} agentName - Agent identifier (e.g. "claude-code").
|
||||
* @returns {object} New object (source is not mutated).
|
||||
*/
|
||||
export function makePackageJsonContent(sourcePkg, skillName, agentName) {
|
||||
return {
|
||||
...sourcePkg,
|
||||
name: `@ai-coding-skills/${skillName}-${agentName}`,
|
||||
private: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Generated-root list ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return the authoritative list of generated roots as repo-relative paths.
|
||||
* Callers (verify-generated.mjs) use this to know which directories to walk.
|
||||
*
|
||||
* @returns {string[]} Sorted array of relative paths.
|
||||
*/
|
||||
export function getGeneratedRoots() {
|
||||
return [...GENERATED_ROOTS];
|
||||
}
|
||||
|
||||
// ── Manifest construction ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Walk a directory recursively and return all file paths (abs).
|
||||
* Skips node_modules.
|
||||
*/
|
||||
async function walkDir(dir) {
|
||||
const results = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const sub = await walkDir(full);
|
||||
results.push(...sub);
|
||||
} else if (entry.isFile()) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a .generated-manifest.json object for a generated root.
|
||||
*
|
||||
* The manifest lists every file in the generated root EXCEPT itself.
|
||||
* Files are sorted by relative path for stable canonical serialization.
|
||||
*
|
||||
* @param {string} generatedRootAbs - Absolute path to the generated root directory.
|
||||
* @param {string} generatedRootRel - Repo-relative path (e.g. "skills/create-plan/pi").
|
||||
* @returns {Promise<object>} Manifest object (not yet serialized to disk).
|
||||
*/
|
||||
export async function buildManifest(generatedRootAbs, generatedRootRel) {
|
||||
const allFiles = await walkDir(generatedRootAbs);
|
||||
const entries = [];
|
||||
|
||||
for (const absPath of allFiles) {
|
||||
const relPath = path.relative(generatedRootAbs, absPath).replace(/\\/g, "/");
|
||||
|
||||
// Non-self-referential: the manifest never lists itself
|
||||
if (relPath === MANIFEST_FILENAME) continue;
|
||||
|
||||
const contentBuf = await readFile(absPath);
|
||||
const sha256 = crypto.createHash("sha256").update(contentBuf).digest("hex");
|
||||
|
||||
const st = await lstat(absPath);
|
||||
const mode = (st.mode & 0o777).toString(8).padStart(3, "0");
|
||||
|
||||
entries.push({
|
||||
path: relPath,
|
||||
kind: "file",
|
||||
mode,
|
||||
sha256,
|
||||
});
|
||||
}
|
||||
|
||||
entries.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
return {
|
||||
$schema: MANIFEST_SCHEMA,
|
||||
generator: MANIFEST_GENERATOR,
|
||||
generatedRoot: generatedRootRel,
|
||||
files: entries,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Core generation helpers ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Copy a single file from source to destination, inserting a header.
|
||||
* Skips header for:
|
||||
* - pnpm-lock.yaml (managed by pnpm, would be stripped on next install)
|
||||
* - JSON files (per header policy, no in-file header)
|
||||
* - node_modules (never copied)
|
||||
*/
|
||||
async function copyWithHeader(srcAbs, dstAbs, canonicalHint) {
|
||||
const basename = path.basename(srcAbs);
|
||||
// Skip pnpm-lock.yaml header (pnpm regenerates without comment)
|
||||
const skipHeader = basename === "pnpm-lock.yaml";
|
||||
|
||||
const raw = await readFile(srcAbs, "utf8");
|
||||
const fileType = skipHeader ? "unknown" : detectFileType(srcAbs);
|
||||
const content = applyHeader(raw, fileType, canonicalHint);
|
||||
|
||||
await mkdir(path.dirname(dstAbs), { recursive: true });
|
||||
await writeFile(dstAbs, content, "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively copy a directory tree, adding headers to text files.
|
||||
*/
|
||||
async function copyDirWithHeaders(srcDir, dstDir, canonicalHint) {
|
||||
const entries = await readdir(srcDir, { withFileTypes: true });
|
||||
await mkdir(dstDir, { recursive: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue;
|
||||
const src = path.join(srcDir, entry.name);
|
||||
const dst = path.join(dstDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await copyDirWithHeaders(src, dst, canonicalHint);
|
||||
} else if (entry.isFile()) {
|
||||
await copyWithHeader(src, dst, canonicalHint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a .generated-manifest.json file into a generated root.
|
||||
*/
|
||||
async function writeManifest(generatedRootAbs, generatedRootRel) {
|
||||
const manifest = await buildManifest(generatedRootAbs, generatedRootRel);
|
||||
const dstPath = path.join(generatedRootAbs, MANIFEST_FILENAME);
|
||||
await writeFile(dstPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
||||
}
|
||||
|
||||
// ── Skill-family generators ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate one agent variant for a "skills-only" skill
|
||||
* (create-plan, do-task, implement-plan).
|
||||
*
|
||||
* Canonical source: skills/<skill>/_source/<agent>/
|
||||
* Generated root: skills/<skill>/<agent>/
|
||||
*
|
||||
* @param {string} writeRoot - Root directory to write output into (defaults to repoRoot).
|
||||
*/
|
||||
async function generateSkillOnlyVariant(repoRoot, writeRoot, skillName, agentName, generatedRootRel) {
|
||||
const sourceDir = path.join(repoRoot, "skills", skillName, "_source", agentName);
|
||||
const targetDir = path.join(writeRoot, generatedRootRel);
|
||||
const canonicalHint = `skills/${skillName}/_source/${agentName}/`;
|
||||
|
||||
// Clear previous generated content (preserve node_modules if any)
|
||||
await clearGeneratedRoot(targetDir);
|
||||
|
||||
// Copy all files from source with headers
|
||||
await copyDirWithHeaders(sourceDir, targetDir, canonicalHint);
|
||||
|
||||
// Write manifest
|
||||
await writeManifest(targetDir, generatedRootRel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate one agent variant for a "scripts+skill" skill
|
||||
* (atlassian, web-automation).
|
||||
*
|
||||
* For atlassian:
|
||||
* - SKILL.md from skills/atlassian/_source/<agent>/SKILL.md
|
||||
* - scripts/* from skills/atlassian/shared/scripts/
|
||||
* (only src/, tsconfig.json, pnpm-lock.yaml — not tests/ or scripts/sync-*)
|
||||
*
|
||||
* For web-automation:
|
||||
* - SKILL.md from skills/web-automation/_source/<agent>/SKILL.md
|
||||
* - scripts/* from skills/web-automation/shared/
|
||||
*
|
||||
* @param {string} [packageAgentName] - Override the agent name used only for the
|
||||
* package.json `name` field. Defaults to `agentName`. Use this to give
|
||||
* pi-package mirrors a distinct name (e.g. "pi-mirror") so workspace package
|
||||
* names are unique even when two roots share the same source agent.
|
||||
*/
|
||||
async function generateScriptsSkillVariant(
|
||||
repoRoot,
|
||||
writeRoot,
|
||||
skillName,
|
||||
agentName,
|
||||
generatedRootRel,
|
||||
config,
|
||||
packageAgentName,
|
||||
) {
|
||||
const targetDir = path.join(writeRoot, generatedRootRel);
|
||||
await clearGeneratedRoot(targetDir);
|
||||
|
||||
// 1. Copy SKILL.md from per-agent canonical source
|
||||
const skillMdSrc = path.join(repoRoot, "skills", skillName, "_source", agentName, "SKILL.md");
|
||||
const skillMdDst = path.join(targetDir, "SKILL.md");
|
||||
const skillMdHint = `skills/${skillName}/_source/${agentName}/SKILL.md`;
|
||||
await copyWithHeader(skillMdSrc, skillMdDst, skillMdHint);
|
||||
|
||||
// 2. Copy scripts
|
||||
const scriptsTargetDir = path.join(targetDir, "scripts");
|
||||
await mkdir(scriptsTargetDir, { recursive: true });
|
||||
|
||||
const canonicalScripts = path.join(repoRoot, config.canonicalScripts);
|
||||
const scriptsHint = `${config.canonicalScripts}/`;
|
||||
|
||||
for (const entry of config.scriptFiles) {
|
||||
const srcPath = path.join(canonicalScripts, entry);
|
||||
const dstPath = path.join(scriptsTargetDir, entry); // scriptsTargetDir already uses writeRoot
|
||||
|
||||
// Check if source exists
|
||||
let st;
|
||||
try {
|
||||
st = await lstat(srcPath);
|
||||
} catch {
|
||||
continue; // skip if file doesn't exist in canonical
|
||||
}
|
||||
|
||||
if (st.isDirectory()) {
|
||||
await copyDirWithHeaders(srcPath, dstPath, scriptsHint);
|
||||
} else {
|
||||
await copyWithHeader(srcPath, dstPath, scriptsHint);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Generate modified package.json
|
||||
const srcPkgPath = path.join(canonicalScripts, "package.json");
|
||||
const srcPkg = JSON.parse(await readFile(srcPkgPath, "utf8"));
|
||||
|
||||
let targetPkg = makePackageJsonContent(srcPkg, skillName, packageAgentName ?? agentName);
|
||||
|
||||
// For atlassian variants, strip test/sync scripts (not in agent variants)
|
||||
if (skillName === "atlassian") {
|
||||
const agentScripts = {};
|
||||
for (const [k, v] of Object.entries(targetPkg.scripts ?? {})) {
|
||||
if (k !== "test" && k !== "sync:agents") agentScripts[k] = v;
|
||||
}
|
||||
targetPkg = { ...targetPkg, scripts: agentScripts };
|
||||
}
|
||||
|
||||
const dstPkgPath = path.join(scriptsTargetDir, "package.json");
|
||||
await writeFile(dstPkgPath, JSON.stringify(targetPkg, null, 2) + "\n", "utf8");
|
||||
|
||||
// 4. Write manifest
|
||||
await writeManifest(targetDir, generatedRootRel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the reviewer-runtime pi variant from the non-Pi canonical scripts.
|
||||
*
|
||||
* Canonical source: skills/reviewer-runtime/{run-review.sh,notify-telegram.sh}
|
||||
* Generated root: skills/reviewer-runtime/pi/
|
||||
*
|
||||
* The pi variant is byte-identical to the canonical except for:
|
||||
* - A generated # comment after the shebang (replaces old "keep in sync" comment)
|
||||
*
|
||||
* @param {string} writeRoot - Root directory to write output into (defaults to repoRoot).
|
||||
*/
|
||||
async function generateReviewerRuntimePi(repoRoot, writeRoot) {
|
||||
const srcDir = path.join(repoRoot, "skills", "reviewer-runtime");
|
||||
const dstDir = path.join(writeRoot, "skills", "reviewer-runtime", "pi");
|
||||
const canonicalHint = "skills/reviewer-runtime/";
|
||||
|
||||
// Clear old generated content (preserve tests/ which is canonical)
|
||||
await clearGeneratedRoot(dstDir);
|
||||
|
||||
for (const fname of ["run-review.sh", "notify-telegram.sh"]) {
|
||||
const srcPath = path.join(srcDir, fname);
|
||||
const dstPath = path.join(dstDir, fname);
|
||||
|
||||
const raw = await readFile(srcPath, "utf8");
|
||||
let content = applyHeader(raw, "shell", `${canonicalHint}${fname}`);
|
||||
|
||||
await mkdir(path.dirname(dstPath), { recursive: true });
|
||||
await writeFile(dstPath, content, "utf8");
|
||||
|
||||
// Preserve executable bit
|
||||
const st = await lstat(srcPath);
|
||||
if (st.mode & 0o100) {
|
||||
const { chmod } = await import("node:fs/promises");
|
||||
await chmod(dstPath, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
// Write manifest (generatedRootRel is always relative to repo root, not writeRoot)
|
||||
await writeManifest(dstDir, "skills/reviewer-runtime/pi");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear generated content in a root, preserving:
|
||||
* - node_modules (installed by pnpm)
|
||||
* - .generated-manifest.json (will be rewritten after generation)
|
||||
*/
|
||||
async function clearGeneratedRoot(rootDir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(rootDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return; // dir doesn't exist yet — nothing to clear
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name === "node_modules") continue;
|
||||
if (entry.name === MANIFEST_FILENAME) continue;
|
||||
await rm(path.join(rootDir, entry.name), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Skill configurations ──────────────────────────────────────────────────
|
||||
|
||||
const SCRIPTS_SKILL_CONFIGS = {
|
||||
atlassian: {
|
||||
canonicalScripts: "skills/atlassian/shared/scripts",
|
||||
// Files to copy from canonicalScripts into each agent's scripts/ dir
|
||||
scriptFiles: ["src", "tsconfig.json", "pnpm-lock.yaml"],
|
||||
},
|
||||
"web-automation": {
|
||||
canonicalScripts: "skills/web-automation/shared",
|
||||
scriptFiles: [
|
||||
"auth.ts",
|
||||
"browse.ts",
|
||||
"check-install.js",
|
||||
"extract.js",
|
||||
"flow.ts",
|
||||
"scan-local-app.ts",
|
||||
"scrape.ts",
|
||||
"test-full.ts",
|
||||
"test-minimal.ts",
|
||||
"test-profile.ts",
|
||||
"tsconfig.json",
|
||||
"turndown-plugin-gfm.d.ts",
|
||||
"pnpm-lock.yaml",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ── Main generator ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Regenerate all agent variants from canonical sources.
|
||||
*
|
||||
* @param {string} repoRoot - Absolute path to repo root (canonical sources are read from here).
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.dryRun] - If true, don't write files (future use).
|
||||
* @param {string} [options.targetRoot] - Write generated output here instead of `repoRoot`.
|
||||
* Canonical sources are always read from `repoRoot`. Use this to generate into a
|
||||
* temp directory for drift detection without modifying on-disk files.
|
||||
* @returns {Promise<{generatedRoots: string[]}>}
|
||||
*/
|
||||
export async function generateSkills(repoRoot = REPO_ROOT, options = {}) {
|
||||
const { dryRun = false, targetRoot } = options;
|
||||
const writeRoot = targetRoot ?? repoRoot;
|
||||
if (dryRun) {
|
||||
return { generatedRoots: GENERATED_ROOTS };
|
||||
}
|
||||
|
||||
const skillOnlySkills = ["create-plan", "do-task", "implement-plan"];
|
||||
|
||||
// 1. Generate skill-only skills (create-plan, do-task, implement-plan)
|
||||
for (const skillName of skillOnlySkills) {
|
||||
for (const agentName of AGENTS) {
|
||||
const rootRel = `skills/${skillName}/${agentName}`;
|
||||
await generateSkillOnlyVariant(repoRoot, writeRoot, skillName, agentName, rootRel);
|
||||
}
|
||||
// pi-package mirror (same source as pi variant)
|
||||
const piPackageRel = `pi-package/skills/${skillName}`;
|
||||
await generateSkillOnlyVariant(repoRoot, writeRoot, skillName, "pi", piPackageRel);
|
||||
}
|
||||
|
||||
// 2. Generate scripts skills (atlassian, web-automation)
|
||||
for (const [skillName, config] of Object.entries(SCRIPTS_SKILL_CONFIGS)) {
|
||||
for (const agentName of AGENTS) {
|
||||
const rootRel = `skills/${skillName}/${agentName}`;
|
||||
await generateScriptsSkillVariant(repoRoot, writeRoot, skillName, agentName, rootRel, config);
|
||||
}
|
||||
// pi-package mirror: same source as pi variant but a distinct package name
|
||||
// ("pi-mirror" suffix) so the workspace has no duplicate package names.
|
||||
const piPackageRel = `pi-package/skills/${skillName}`;
|
||||
await generateScriptsSkillVariant(repoRoot, writeRoot, skillName, "pi", piPackageRel, config, "pi-mirror");
|
||||
}
|
||||
|
||||
// 3. Generate reviewer-runtime pi variant
|
||||
await generateReviewerRuntimePi(repoRoot, writeRoot);
|
||||
|
||||
return { generatedRoots: GENERATED_ROOTS };
|
||||
}
|
||||
|
||||
// ── CLI entry point ────────────────────────────────────────────────────────
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
try {
|
||||
const result = await generateSkills(REPO_ROOT);
|
||||
console.log(
|
||||
`Generated ${result.generatedRoots.length} roots from canonical sources.`,
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("generate-skills: fatal error:", err.message ?? err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user