* chore: upgrade maintenance scripts to robust PyYAML parsing - Replaces fragile regex frontmatter parsing with PyYAML/yaml library - Ensures multi-line descriptions and complex characters are handled safely - Normalizes quoting and field ordering across all maintenance scripts - Updates validator to strictly enforce description quality * fix: restore and refine truncated skill descriptions - Recovered 223+ truncated descriptions from git history (6.5.0 regression) - Refined long descriptions into concise, complete sentences (<200 chars) - Added missing descriptions for brainstorming and orchestration skills - Manually fixed imagen skill description - Resolved dangling links in competitor-alternatives skill * chore: sync generated registry files and document fixes - Regenerated skills index with normalized forward-slash paths - Updated README and CATALOG to reflect restored descriptions - Documented restoration and script improvements in CHANGELOG.md * fix: restore missing skill and align metadata for full 955 count - Renamed SKILL.MD to SKILL.md in andruia-skill-smith to ensure indexing - Fixed risk level and missing section in andruia-skill-smith - Synchronized all registry files for final 955 skill count * chore(scripts): add cross-platform runners and hermetic test orchestration * fix(scripts): harden utf-8 output and clone target writeability * fix(skills): add missing date metadata for strict validation * chore(index): sync generated metadata dates * fix(catalog): normalize skill paths to prevent CI drift * chore: sync generated registry files * fix: enforce LF line endings for generated registry files
156 lines
4.3 KiB
JavaScript
156 lines
4.3 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const yaml = require('yaml');
|
|
const { listSkillIds, parseFrontmatter } = require('../lib/skill-utils');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const SKILLS_DIR = path.join(ROOT, 'skills');
|
|
const ALLOWED_FIELDS = new Set([
|
|
'name',
|
|
'description',
|
|
'risk',
|
|
'source',
|
|
'license',
|
|
'compatibility',
|
|
'metadata',
|
|
'allowed-tools',
|
|
'date_added',
|
|
'category',
|
|
'id',
|
|
]);
|
|
|
|
function isPlainObject(value) {
|
|
return value && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function coerceToString(value) {
|
|
if (value === null || value === undefined) return '';
|
|
if (typeof value === 'string') return value.trim();
|
|
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
if (Array.isArray(value)) {
|
|
const simple = value.every(item => ['string', 'number', 'boolean'].includes(typeof item));
|
|
return simple ? value.map(item => String(item).trim()).filter(Boolean).join(', ') : JSON.stringify(value);
|
|
}
|
|
if (isPlainObject(value)) {
|
|
return JSON.stringify(value);
|
|
}
|
|
return String(value).trim();
|
|
}
|
|
|
|
function appendMetadata(metadata, key, value) {
|
|
const nextValue = coerceToString(value);
|
|
if (!nextValue) return;
|
|
if (!metadata[key]) {
|
|
metadata[key] = nextValue;
|
|
return;
|
|
}
|
|
if (metadata[key].includes(nextValue)) return;
|
|
metadata[key] = `${metadata[key]}, ${nextValue}`;
|
|
}
|
|
|
|
function collectAllowedTools(value, toolSet) {
|
|
if (!value) return;
|
|
if (typeof value === 'string') {
|
|
value
|
|
.split(/[\s,]+/)
|
|
.map(token => token.trim())
|
|
.filter(Boolean)
|
|
.forEach(token => toolSet.add(token));
|
|
return;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
value
|
|
.map(token => String(token).trim())
|
|
.filter(Boolean)
|
|
.forEach(token => toolSet.add(token));
|
|
}
|
|
}
|
|
|
|
function normalizeSkill(skillId) {
|
|
const skillPath = path.join(SKILLS_DIR, skillId, 'SKILL.md');
|
|
const content = fs.readFileSync(skillPath, 'utf8');
|
|
const { data, body, hasFrontmatter } = parseFrontmatter(content);
|
|
|
|
if (!hasFrontmatter) return false;
|
|
|
|
let modified = false;
|
|
const updated = { ...data };
|
|
const metadata = isPlainObject(updated.metadata) ? { ...updated.metadata } : {};
|
|
if (updated.metadata !== undefined && !isPlainObject(updated.metadata)) {
|
|
appendMetadata(metadata, 'legacy_metadata', updated.metadata);
|
|
modified = true;
|
|
}
|
|
|
|
const allowedTools = new Set();
|
|
collectAllowedTools(updated['allowed-tools'], allowedTools);
|
|
collectAllowedTools(updated.tools, allowedTools);
|
|
collectAllowedTools(updated.tool_access, allowedTools);
|
|
|
|
if (updated.tools !== undefined) {
|
|
delete updated.tools;
|
|
modified = true;
|
|
}
|
|
if (updated.tool_access !== undefined) {
|
|
delete updated.tool_access;
|
|
modified = true;
|
|
}
|
|
|
|
for (const key of Object.keys(updated)) {
|
|
if (ALLOWED_FIELDS.has(key)) continue;
|
|
if (key === 'tags') {
|
|
appendMetadata(metadata, 'tags', updated[key]);
|
|
} else {
|
|
appendMetadata(metadata, key, updated[key]);
|
|
}
|
|
delete updated[key];
|
|
modified = true;
|
|
}
|
|
|
|
if (allowedTools.size) {
|
|
updated['allowed-tools'] = Array.from(allowedTools).join(' ');
|
|
modified = true;
|
|
} else if (updated['allowed-tools'] !== undefined) {
|
|
delete updated['allowed-tools'];
|
|
modified = true;
|
|
}
|
|
|
|
if (Object.keys(metadata).length) {
|
|
updated.metadata = metadata;
|
|
modified = true;
|
|
} else if (updated.metadata !== undefined) {
|
|
delete updated.metadata;
|
|
modified = true;
|
|
}
|
|
|
|
if (!modified) return false;
|
|
|
|
const ordered = {};
|
|
const order = ['id', 'name', 'description', 'category', 'risk', 'source', 'license', 'compatibility', 'date_added', 'allowed-tools', 'metadata'];
|
|
for (const key of order) {
|
|
if (updated[key] !== undefined) {
|
|
ordered[key] = updated[key];
|
|
}
|
|
}
|
|
|
|
const fm = yaml.stringify(ordered).trimEnd();
|
|
const bodyPrefix = body.length && (body.startsWith('\n') || body.startsWith('\r\n')) ? '' : '\n';
|
|
const next = `---\n${fm}\n---${bodyPrefix}${body}`;
|
|
fs.writeFileSync(skillPath, next);
|
|
return true;
|
|
}
|
|
|
|
function run() {
|
|
const skillIds = listSkillIds(SKILLS_DIR);
|
|
let updatedCount = 0;
|
|
for (const skillId of skillIds) {
|
|
if (normalizeSkill(skillId)) updatedCount += 1;
|
|
}
|
|
console.log(`Normalized frontmatter for ${updatedCount} skills.`);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
run();
|
|
}
|
|
|
|
module.exports = { run };
|