mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-23 20:05:12 +00:00
New awesome agent primitive
This commit is contained in:
172
eng/validate-skills.mjs
Normal file
172
eng/validate-skills.mjs
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { parseSkillMetadata } from "./yaml-parser.mjs";
|
||||
import {
|
||||
ROOT_FOLDER,
|
||||
SKILLS_DIR,
|
||||
SKILL_NAME_MIN_LENGTH,
|
||||
SKILL_NAME_MAX_LENGTH,
|
||||
SKILL_DESCRIPTION_MIN_LENGTH,
|
||||
SKILL_DESCRIPTION_MAX_LENGTH,
|
||||
} from "./constants.mjs";
|
||||
|
||||
// Validation functions
|
||||
function validateSkillName(name) {
|
||||
if (!name || typeof name !== "string") {
|
||||
return "name is required and must be a string";
|
||||
}
|
||||
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||
return "name must contain only lowercase letters, numbers, and hyphens";
|
||||
}
|
||||
if (name.length < SKILL_NAME_MIN_LENGTH || name.length > SKILL_NAME_MAX_LENGTH) {
|
||||
return `name must be between ${SKILL_NAME_MIN_LENGTH} and ${SKILL_NAME_MAX_LENGTH} characters`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateSkillDescription(description) {
|
||||
if (!description || typeof description !== "string") {
|
||||
return "description is required and must be a string";
|
||||
}
|
||||
if (description.length < SKILL_DESCRIPTION_MIN_LENGTH) {
|
||||
return `description must be at least ${SKILL_DESCRIPTION_MIN_LENGTH} characters`;
|
||||
}
|
||||
if (description.length > SKILL_DESCRIPTION_MAX_LENGTH) {
|
||||
return `description must not exceed ${SKILL_DESCRIPTION_MAX_LENGTH} characters`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateSkillFolder(folderPath, folderName) {
|
||||
const errors = [];
|
||||
|
||||
// Check if SKILL.md exists
|
||||
const skillFile = path.join(folderPath, "SKILL.md");
|
||||
if (!fs.existsSync(skillFile)) {
|
||||
errors.push("Missing SKILL.md file");
|
||||
return errors; // Cannot proceed without SKILL.md
|
||||
}
|
||||
|
||||
// Parse and validate frontmatter
|
||||
const metadata = parseSkillMetadata(folderPath);
|
||||
if (!metadata) {
|
||||
errors.push("Failed to parse SKILL.md frontmatter");
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Validate name field
|
||||
const nameError = validateSkillName(metadata.name);
|
||||
if (nameError) {
|
||||
errors.push(`name: ${nameError}`);
|
||||
} else {
|
||||
// Validate that folder name matches skill name
|
||||
if (metadata.name !== folderName) {
|
||||
errors.push(
|
||||
`Folder name "${folderName}" does not match skill name "${metadata.name}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate description field
|
||||
const descError = validateSkillDescription(metadata.description);
|
||||
if (descError) {
|
||||
errors.push(`description: ${descError}`);
|
||||
}
|
||||
|
||||
// Check for reasonable file sizes in bundled assets
|
||||
const MAX_ASSET_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
for (const asset of metadata.assets) {
|
||||
const assetPath = path.join(folderPath, asset);
|
||||
try {
|
||||
const stats = fs.statSync(assetPath);
|
||||
if (stats.size > MAX_ASSET_SIZE) {
|
||||
errors.push(
|
||||
`Bundled asset "${asset}" exceeds maximum size of 5MB (${(
|
||||
stats.size /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(2)}MB)`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Cannot access bundled asset "${asset}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Main validation function
|
||||
function validateSkills() {
|
||||
if (!fs.existsSync(SKILLS_DIR)) {
|
||||
console.log("No skills directory found - validation skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
const skillFolders = fs
|
||||
.readdirSync(SKILLS_DIR)
|
||||
.filter((file) => {
|
||||
const filePath = path.join(SKILLS_DIR, file);
|
||||
return fs.statSync(filePath).isDirectory();
|
||||
});
|
||||
|
||||
if (skillFolders.length === 0) {
|
||||
console.log("No skill folders found - validation skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Validating ${skillFolders.length} skill folder(s)...`);
|
||||
|
||||
let hasErrors = false;
|
||||
const usedNames = new Set();
|
||||
|
||||
for (const folder of skillFolders) {
|
||||
const folderPath = path.join(SKILLS_DIR, folder);
|
||||
console.log(`\nValidating ${folder}...`);
|
||||
|
||||
const errors = validateSkillFolder(folderPath, folder);
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`❌ Validation errors in ${folder}:`);
|
||||
errors.forEach((error) => console.error(` - ${error}`));
|
||||
hasErrors = true;
|
||||
} else {
|
||||
console.log(`✅ ${folder} is valid`);
|
||||
|
||||
// Check for duplicate names (only if no errors)
|
||||
const metadata = parseSkillMetadata(folderPath);
|
||||
if (metadata) {
|
||||
if (usedNames.has(metadata.name)) {
|
||||
console.error(
|
||||
`❌ Duplicate skill name "${metadata.name}" found in ${folder}`
|
||||
);
|
||||
hasErrors = true;
|
||||
} else {
|
||||
usedNames.add(metadata.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasErrors) {
|
||||
console.log(`\n✅ All ${skillFolders.length} skills are valid`);
|
||||
}
|
||||
|
||||
return !hasErrors;
|
||||
}
|
||||
|
||||
// Run validation
|
||||
try {
|
||||
const isValid = validateSkills();
|
||||
if (!isValid) {
|
||||
console.error("\n❌ Skill validation failed");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\n🎉 Skill validation passed");
|
||||
} catch (error) {
|
||||
console.error(`Error during validation: ${error.message}`);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user