Rebuild the affected vibeship-derived skills from the pinned upstream snapshot instead of leaving the truncated imported bodies on main. Refresh the derived catalog and plugin mirrors so the canonical skills, compatibility data, and generated artifacts stay in sync. Refs #473
540 lines
17 KiB
JavaScript
540 lines
17 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const cp = require("child_process");
|
|
const YAML = require("yaml");
|
|
|
|
const ROOT = process.cwd();
|
|
const UPSTREAM_SHA = "70b2e1062fc6a38fce854226c27097a87732cb5f";
|
|
const SOURCE_LABEL = "vibeship-spawner-skills (Apache 2.0)";
|
|
const LIST_PATH = "/tmp/vibeship_files.txt";
|
|
const FILES = fs.existsSync(LIST_PATH)
|
|
? fs.readFileSync(LIST_PATH, "utf8").trim().split("\n").filter(Boolean)
|
|
: [];
|
|
const TREE = JSON.parse(
|
|
runCommand(
|
|
`gh api 'repos/vibeforge1111/vibeship-spawner-skills/git/trees/${UPSTREAM_SHA}?recursive=1'`,
|
|
),
|
|
);
|
|
const SKILL_PATHS = TREE.tree
|
|
.filter((entry) => /(^|\/)skill\.yaml$/.test(entry.path))
|
|
.map((entry) => entry.path);
|
|
|
|
function runCommand(cmd) {
|
|
return cp.execSync(cmd, {
|
|
encoding: "utf8",
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
cwd: ROOT,
|
|
});
|
|
}
|
|
|
|
function fetchText(url) {
|
|
return runCommand(`curl -fsSL --max-time 30 ${JSON.stringify(url)}`);
|
|
}
|
|
|
|
function parseOptionalYaml(relPath) {
|
|
const url = `https://raw.githubusercontent.com/vibeforge1111/vibeship-spawner-skills/${UPSTREAM_SHA}/${relPath}`;
|
|
try {
|
|
return YAML.parse(fetchText(url));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function parseFrontmatter(content) {
|
|
if (!content.startsWith("---\n")) {
|
|
return { data: {}, body: content };
|
|
}
|
|
|
|
const end = content.indexOf("\n---\n", 4);
|
|
if (end === -1) {
|
|
return { data: {}, body: content };
|
|
}
|
|
|
|
return {
|
|
data: YAML.parse(content.slice(4, end)) || {},
|
|
body: content.slice(end + 5),
|
|
};
|
|
}
|
|
|
|
function stringifyFrontmatter(data) {
|
|
return `---\n${YAML.stringify(data).trimEnd()}\n---\n`;
|
|
}
|
|
|
|
function sanitizeText(text) {
|
|
return String(text || "")
|
|
.replace(/\r/g, "")
|
|
.replace(/\/Users\/yourname\//g, "~/")
|
|
.replace(/\/Users\/username\//g, "~/")
|
|
.replace(/C:\/Users\/yourname\//g, "%USERPROFILE%/")
|
|
.replace(/C:\/Users\/username\//g, "%USERPROFILE%/");
|
|
}
|
|
|
|
function clean(text) {
|
|
return sanitizeText(String(text || "")).trim();
|
|
}
|
|
|
|
function isScalar(value) {
|
|
return ["string", "number", "boolean"].includes(typeof value);
|
|
}
|
|
|
|
function formatInline(value) {
|
|
if (value === null || value === undefined || value === "") return "";
|
|
if (isScalar(value)) return clean(value);
|
|
if (Array.isArray(value)) {
|
|
const rendered = value.map((entry) => formatInline(entry)).filter(Boolean);
|
|
return rendered.join(", ");
|
|
}
|
|
if (typeof value === "object") {
|
|
const preferredKeys = [
|
|
"name",
|
|
"title",
|
|
"role",
|
|
"trigger",
|
|
"skill",
|
|
"id",
|
|
"description",
|
|
"summary",
|
|
"context",
|
|
"action",
|
|
"pattern",
|
|
"severity",
|
|
"provides",
|
|
"receives",
|
|
];
|
|
const orderedKeys = [
|
|
...preferredKeys.filter((key) => key in value),
|
|
...Object.keys(value).filter((key) => !preferredKeys.includes(key)),
|
|
];
|
|
const parts = [];
|
|
for (const key of orderedKeys) {
|
|
const rendered = formatInline(value[key]);
|
|
if (!rendered) continue;
|
|
if (["name", "title", "role", "trigger", "skill", "id"].includes(key)) {
|
|
parts.push(rendered);
|
|
} else {
|
|
parts.push(`${titleize(key)}: ${rendered}`);
|
|
}
|
|
}
|
|
return parts.join(" | ");
|
|
}
|
|
return clean(String(value));
|
|
}
|
|
|
|
function renderMarkdown(text) {
|
|
return sanitizeText(String(text || "")).replace(/\r/g, "").trim();
|
|
}
|
|
|
|
function titleize(slug) {
|
|
return String(slug || "")
|
|
.split("-")
|
|
.filter(Boolean)
|
|
.map((part) => {
|
|
const lower = part.toLowerCase();
|
|
if (lower === "ai") return "AI";
|
|
if (lower === "llm") return "LLM";
|
|
if (part.toUpperCase() === part && part.length <= 5) return part;
|
|
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
})
|
|
.join(" ");
|
|
}
|
|
|
|
function summarizeDescription(raw) {
|
|
const cleaned = clean(raw).replace(/\n+/g, " ").replace(/\s+/g, " ").trim();
|
|
if (cleaned.length <= 280) return cleaned;
|
|
|
|
const sentences = cleaned.match(/[^.!?]+[.!?]+/g);
|
|
if (sentences) {
|
|
let acc = "";
|
|
for (const sentence of sentences) {
|
|
const next = (acc ? `${acc} ` : "") + sentence.trim();
|
|
if (next.length > 280) break;
|
|
acc = next;
|
|
}
|
|
if (acc) return acc;
|
|
}
|
|
|
|
return `${cleaned.slice(0, 277).trimEnd()}...`;
|
|
}
|
|
|
|
function bullets(items) {
|
|
return items
|
|
.map((item) => formatInline(item))
|
|
.filter(Boolean)
|
|
.map((item) => `- ${item}`)
|
|
.join("\n");
|
|
}
|
|
|
|
function codeBlock(text) {
|
|
return ["```", clean(text), "```"].join("\n");
|
|
}
|
|
|
|
function objectBullets(obj, indent = "") {
|
|
const lines = [];
|
|
|
|
for (const [key, value] of Object.entries(obj || {})) {
|
|
const label = titleize(key);
|
|
|
|
if (Array.isArray(value)) {
|
|
if (!value.length) continue;
|
|
|
|
if (value.every((entry) => typeof entry === "string")) {
|
|
lines.push(`${indent}- ${label}: ${value.join(", ")}`);
|
|
continue;
|
|
}
|
|
|
|
lines.push(`${indent}- ${label}:`);
|
|
for (const entry of value) {
|
|
if (typeof entry === "string") {
|
|
lines.push(`${indent} - ${entry}`);
|
|
continue;
|
|
}
|
|
|
|
if (!entry || typeof entry !== "object") continue;
|
|
const parts = [];
|
|
for (const [entryKey, entryValue] of Object.entries(entry)) {
|
|
if (entryValue === null || entryValue === undefined || entryValue === "") continue;
|
|
parts.push(
|
|
`${entryKey}: ${
|
|
typeof entryValue === "string" ? entryValue : JSON.stringify(entryValue)
|
|
}`,
|
|
);
|
|
}
|
|
lines.push(`${indent} - ${parts.join(" | ")}`);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (value && typeof value === "object") {
|
|
lines.push(`${indent}- ${label}:`);
|
|
lines.push(objectBullets(value, `${indent} `));
|
|
continue;
|
|
}
|
|
|
|
if (value !== null && value !== undefined && value !== "") {
|
|
lines.push(`${indent}- ${label}: ${value}`);
|
|
}
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function renderToolingSection(title, obj) {
|
|
if (!obj || typeof obj !== "object") return null;
|
|
|
|
const parts = [];
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (!value || (Array.isArray(value) && !value.length)) continue;
|
|
|
|
parts.push(`### ${titleize(key)}`);
|
|
if (Array.isArray(value)) {
|
|
const rows = value
|
|
.map((entry) => {
|
|
if (typeof entry === "string") return `- ${entry}`;
|
|
if (!entry || typeof entry !== "object") return null;
|
|
|
|
const label = entry.name || entry.skill || entry.id || "Item";
|
|
const details = [];
|
|
if (entry.when) details.push(`When: ${entry.when}`);
|
|
if (entry.note) details.push(`Note: ${entry.note}`);
|
|
if (entry.description) details.push(entry.description);
|
|
return `- ${label}${details.length ? ` - ${details.join(" ")}` : ""}`;
|
|
})
|
|
.filter(Boolean);
|
|
|
|
if (rows.length) {
|
|
parts.push(rows.join("\n"));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (typeof value === "object") {
|
|
parts.push(objectBullets(value));
|
|
continue;
|
|
}
|
|
|
|
parts.push(String(value));
|
|
}
|
|
|
|
if (!parts.length) return null;
|
|
return `## ${title}\n\n${parts.join("\n\n")}`;
|
|
}
|
|
|
|
function renderIdentity(identity) {
|
|
if (!identity || typeof identity !== "object") return null;
|
|
|
|
const parts = [];
|
|
if (identity.role) parts.push(`**Role**: ${clean(identity.role)}`);
|
|
if (identity.personality) parts.push(renderMarkdown(identity.personality));
|
|
if (Array.isArray(identity.expertise) && identity.expertise.length) {
|
|
parts.push(`### Expertise\n\n${bullets(identity.expertise)}`);
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(identity)) {
|
|
if (["role", "personality", "expertise"].includes(key)) continue;
|
|
if (!value || (Array.isArray(value) && !value.length)) continue;
|
|
if (typeof value === "string") {
|
|
parts.push(`### ${titleize(key)}\n\n${renderMarkdown(value)}`);
|
|
} else if (Array.isArray(value)) {
|
|
parts.push(`### ${titleize(key)}\n\n${bullets(value)}`);
|
|
} else if (typeof value === "object") {
|
|
parts.push(`### ${titleize(key)}\n\n${objectBullets(value)}`);
|
|
}
|
|
}
|
|
|
|
if (!parts.length) return null;
|
|
return parts.join("\n\n");
|
|
}
|
|
|
|
function renderPatterns(patterns) {
|
|
if (!Array.isArray(patterns) || !patterns.length) return null;
|
|
|
|
const blocks = patterns.map((pattern) => {
|
|
const lines = [`### ${pattern.name || pattern.id || "Pattern"}`];
|
|
if (pattern.description) lines.push("", clean(pattern.description));
|
|
const whenToUse = pattern.when_to_use || pattern.when;
|
|
if (whenToUse) lines.push("", `**When to use**: ${clean(whenToUse)}`);
|
|
|
|
const implementation = pattern.implementation || pattern.example;
|
|
if (implementation) lines.push("", renderMarkdown(implementation));
|
|
|
|
for (const [key, value] of Object.entries(pattern)) {
|
|
if (
|
|
["name", "id", "description", "when", "when_to_use", "implementation", "example"].includes(
|
|
key,
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
if (!value || (Array.isArray(value) && !value.length)) continue;
|
|
if (typeof value === "string") {
|
|
lines.push("", `### ${titleize(key)}`, "", renderMarkdown(value));
|
|
} else if (Array.isArray(value)) {
|
|
lines.push("", `### ${titleize(key)}`, "", bullets(value));
|
|
} else if (typeof value === "object") {
|
|
lines.push("", `### ${titleize(key)}`, "", objectBullets(value));
|
|
}
|
|
}
|
|
return lines.join("\n");
|
|
});
|
|
|
|
return `## Patterns\n\n${blocks.join("\n\n")}`;
|
|
}
|
|
|
|
function renderSharpEdges(data) {
|
|
const edges = data && Array.isArray(data.sharp_edges) ? data.sharp_edges : null;
|
|
if (!edges || !edges.length) return null;
|
|
|
|
const blocks = edges.map((edge) => {
|
|
const lines = [`### ${edge.title || edge.summary || edge.id || "Sharp Edge"}`];
|
|
if (edge.severity) lines.push("", `Severity: ${String(edge.severity).toUpperCase()}`);
|
|
if (edge.situation) lines.push("", `Situation: ${clean(edge.situation)}`);
|
|
if (edge.symptom) lines.push("", "Symptoms:", clean(edge.symptom));
|
|
if (Array.isArray(edge.symptoms) && edge.symptoms.length) {
|
|
lines.push("", "Symptoms:", bullets(edge.symptoms));
|
|
}
|
|
if (edge.why) lines.push("", "Why this breaks:", clean(edge.why));
|
|
if (edge.solution) lines.push("", "Recommended fix:", "", renderMarkdown(edge.solution));
|
|
return lines.join("\n");
|
|
});
|
|
|
|
return `## Sharp Edges\n\n${blocks.join("\n\n")}`;
|
|
}
|
|
|
|
function renderValidations(data) {
|
|
const validations = data && Array.isArray(data.validations) ? data.validations : null;
|
|
if (!validations || !validations.length) return null;
|
|
|
|
const blocks = validations.slice(0, 10).map((entry) => {
|
|
const lines = [`### ${entry.name || entry.id || "Validation"}`];
|
|
if (entry.severity) lines.push("", `Severity: ${String(entry.severity).toUpperCase()}`);
|
|
if (entry.description) lines.push("", clean(entry.description));
|
|
if (entry.message) lines.push("", `Message: ${clean(entry.message)}`);
|
|
if (entry.fix_action) lines.push("", `Fix action: ${clean(entry.fix_action)}`);
|
|
return lines.join("\n");
|
|
});
|
|
|
|
return `## Validation Checks\n\n${blocks.join("\n\n")}`;
|
|
}
|
|
|
|
function renderCollaboration(data) {
|
|
if (!data || typeof data !== "object") return null;
|
|
|
|
const parts = [];
|
|
if (Array.isArray(data.delegation_triggers) && data.delegation_triggers.length) {
|
|
const rows = data.delegation_triggers.map(
|
|
(entry) =>
|
|
`- ${entry.trigger} -> ${entry.delegate_to}${
|
|
entry.context ? ` (${entry.context})` : ""
|
|
}`,
|
|
);
|
|
parts.push(`### Delegation Triggers\n\n${rows.join("\n")}`);
|
|
}
|
|
|
|
if (Array.isArray(data.common_combinations) && data.common_combinations.length) {
|
|
const combos = data.common_combinations.map((entry) => {
|
|
const lines = [`### ${entry.name || "Combination"}`];
|
|
if (Array.isArray(entry.skills) && entry.skills.length) {
|
|
lines.push("", `Skills: ${entry.skills.join(", ")}`);
|
|
}
|
|
if (entry.workflow) lines.push("", "Workflow:", "", codeBlock(entry.workflow));
|
|
return lines.join("\n");
|
|
});
|
|
parts.push(combos.join("\n\n"));
|
|
}
|
|
|
|
if (!parts.length) return null;
|
|
return `## Collaboration\n\n${parts.join("\n\n")}`;
|
|
}
|
|
|
|
function renderWhenToUse(skill) {
|
|
const triggers = Array.isArray(skill.triggers) ? skill.triggers : [];
|
|
if (!triggers.length) {
|
|
return "## When to Use\n\nUse this skill when the request clearly matches the capabilities and patterns described above.";
|
|
}
|
|
return `## When to Use\n\n${bullets(
|
|
triggers.map((trigger) => `User mentions or implies: ${trigger}`),
|
|
)}`;
|
|
}
|
|
|
|
function buildBody(skill, sharp, validations, collaboration) {
|
|
const sections = [];
|
|
|
|
sections.push(`# ${skill.name || titleize(skill.id || "skill")}`);
|
|
if (skill.description) sections.push(clean(skill.description));
|
|
const identitySection = renderIdentity(skill.identity);
|
|
if (identitySection) sections.push(identitySection);
|
|
if (Array.isArray(skill.principles) && skill.principles.length) {
|
|
sections.push(`## Principles\n\n${bullets(skill.principles)}`);
|
|
}
|
|
if (Array.isArray(skill.owns) && skill.owns.length) {
|
|
sections.push(`## Capabilities\n\n${bullets(skill.owns)}`);
|
|
}
|
|
|
|
const prereq = [];
|
|
if (skill.prerequisites && typeof skill.prerequisites === "object") {
|
|
prereq.push(objectBullets(skill.prerequisites));
|
|
}
|
|
if (Array.isArray(skill.requires) && skill.requires.length) {
|
|
prereq.push(`- Required skills: ${skill.requires.join(", ")}`);
|
|
}
|
|
if (prereq.length) sections.push(`## Prerequisites\n\n${prereq.filter(Boolean).join("\n")}`);
|
|
|
|
const scope = [];
|
|
if (skill.limits && typeof skill.limits === "object") {
|
|
scope.push(objectBullets(skill.limits));
|
|
}
|
|
if (skill.does_not_own) {
|
|
if (Array.isArray(skill.does_not_own)) scope.push(bullets(skill.does_not_own));
|
|
else if (typeof skill.does_not_own === "object") scope.push(objectBullets(skill.does_not_own));
|
|
}
|
|
if (scope.length) sections.push(`## Scope\n\n${scope.filter(Boolean).join("\n")}`);
|
|
|
|
const tooling = [
|
|
renderToolingSection("Tooling", skill.stack),
|
|
renderToolingSection("Ecosystem", skill.ecosystem),
|
|
].filter(Boolean);
|
|
if (tooling.length) sections.push(tooling.join("\n\n"));
|
|
|
|
const patterns = renderPatterns(skill.patterns);
|
|
if (patterns) sections.push(patterns);
|
|
|
|
const sharpEdges = renderSharpEdges(sharp);
|
|
if (sharpEdges) sections.push(sharpEdges);
|
|
|
|
const validationChecks = renderValidations(validations);
|
|
if (validationChecks) sections.push(validationChecks);
|
|
|
|
const collaborationSection = renderCollaboration(collaboration);
|
|
if (collaborationSection) sections.push(collaborationSection);
|
|
|
|
const related = Array.isArray(skill.pairs_with) ? skill.pairs_with : [];
|
|
if (related.length) {
|
|
sections.push(
|
|
`## Related Skills\n\nWorks well with: ${related
|
|
.map((name) => "`" + name + "`")
|
|
.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
sections.push(renderWhenToUse(skill));
|
|
|
|
return `${sections.filter(Boolean).join("\n\n")}\n`;
|
|
}
|
|
|
|
function forceUpstreamDescription(absPath, description) {
|
|
const content = fs.readFileSync(absPath, "utf8");
|
|
const parsed = parseFrontmatter(content);
|
|
const next = { ...parsed.data, description: summarizeDescription(description || parsed.data.description || "") };
|
|
fs.writeFileSync(absPath, `${stringifyFrontmatter(next)}\n${parsed.body.replace(/^\n/, "")}`);
|
|
}
|
|
|
|
function loadUpstreamPathBySkillId() {
|
|
const map = new Map();
|
|
for (const upstreamPath of SKILL_PATHS) {
|
|
const skillId = path.posix.basename(path.posix.dirname(upstreamPath));
|
|
if (!map.has(skillId)) map.set(skillId, []);
|
|
map.get(skillId).push(upstreamPath);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
function main() {
|
|
if (!fs.existsSync(LIST_PATH)) {
|
|
throw new Error(`Missing skill list: ${LIST_PATH}`);
|
|
}
|
|
|
|
const skillPathMap = loadUpstreamPathBySkillId();
|
|
const touched = [];
|
|
const skipped = [];
|
|
|
|
for (const rel of FILES) {
|
|
const skillId = rel.split("/")[1];
|
|
const matches = skillPathMap.get(skillId) || [];
|
|
if (matches.length !== 1) {
|
|
skipped.push({ rel, matches });
|
|
continue;
|
|
}
|
|
|
|
const upstreamPath = matches[0];
|
|
const baseDir = path.posix.dirname(upstreamPath);
|
|
const skill = YAML.parse(
|
|
fetchText(
|
|
`https://raw.githubusercontent.com/vibeforge1111/vibeship-spawner-skills/${UPSTREAM_SHA}/${upstreamPath}`,
|
|
),
|
|
);
|
|
const sharp = parseOptionalYaml(`${baseDir}/sharp-edges.yaml`);
|
|
const validations = parseOptionalYaml(`${baseDir}/validations.yaml`);
|
|
const collaboration = parseOptionalYaml(`${baseDir}/collaboration.yaml`);
|
|
|
|
const abs = path.join(ROOT, rel);
|
|
const existing = parseFrontmatter(fs.readFileSync(abs, "utf8"));
|
|
const frontmatter = { ...existing.data };
|
|
frontmatter.name = frontmatter.name || skill.id || skillId;
|
|
frontmatter.description = summarizeDescription(skill.description || existing.data.description || "");
|
|
frontmatter.risk = frontmatter.risk || "unknown";
|
|
frontmatter.source = existing.data.source || SOURCE_LABEL;
|
|
if (existing.data.date_added !== undefined) {
|
|
frontmatter.date_added = existing.data.date_added;
|
|
}
|
|
|
|
fs.writeFileSync(
|
|
abs,
|
|
`${stringifyFrontmatter(frontmatter)}\n${buildBody(skill, sharp, validations, collaboration)}`,
|
|
);
|
|
forceUpstreamDescription(abs, skill.description);
|
|
touched.push(rel);
|
|
}
|
|
|
|
console.log(`Rebuilt ${touched.length} vibeship skill files.`);
|
|
if (skipped.length) {
|
|
console.log("Skipped mappings:");
|
|
for (const entry of skipped) {
|
|
console.log(`- ${entry.rel} (${entry.matches.length} upstream matches)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|