@forge/manifest
13.1.1-next.2-experimental-094961613.2.0-next.5
out/validators/modules-validators/rovo/validate-skill-file.js+
out/validators/modules-validators/rovo/validate-skill-file.jsNew file+175
Index: package/out/validators/modules-validators/rovo/validate-skill-file.js
===================================================================
--- package/out/validators/modules-validators/rovo/validate-skill-file.js
+++ package/out/validators/modules-validators/rovo/validate-skill-file.js
@@ -0,0 +1,175 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.validateSkillFile = void 0;
+const tslib_1 = require("tslib");
+const types_1 = require("../../../types");
+const text_1 = require("../../../text");
+const utils_1 = require("../../../utils");
+const fs_1 = require("fs");
+const path_1 = require("path");
+const yaml_1 = tslib_1.__importDefault(require("yaml"));
+const skill_file_rules_1 = require("./skill-file-rules");
+const referencedFileRegex = /(?:^|[^\w/.-])((?:scripts|references|assets)\/[A-Za-z0-9_./-]+)/g;
+const trailingPunctuationRegex = /[.,:;!?)]$/;
+const extractReferencedBodyFiles = (body) => {
+ const referencedFiles = new Set();
+ for (const match of body.matchAll(referencedFileRegex)) {
+ if (match[1]) {
+ referencedFiles.add(match[1].replace(trailingPunctuationRegex, ''));
+ }
+ }
+ return [...referencedFiles];
+};
+const toValidationError = (skillKey, message, level, yamlContentByLine) => ({
+ message,
+ reference: text_1.References.Modules,
+ level,
+ ...(0, utils_1.findPosition)(skillKey, yamlContentByLine)
+});
+const collectRuleErrors = (context, validators, yamlContentByLine) => validators
+ .filter((validator) => !validator.check(context))
+ .map((validator) => {
+ const { skill: { key } } = context;
+ return toValidationError(key, validator.errorMessageGenerator(context), validator.errorType, yamlContentByLine);
+});
+const resolveSkillFilePath = (skill, manifestDir) => {
+ const sourceDir = skill.source?.dir;
+ if (typeof sourceDir !== 'string' || sourceDir.trim() === '' || sourceDir.startsWith('..')) {
+ return undefined;
+ }
+ const skillDirectory = (0, path_1.resolve)(manifestDir ?? '', sourceDir);
+ const skillFilePath = (0, path_1.resolve)(skillDirectory, 'SKILL.md');
+ if (!(0, fs_1.existsSync)(skillDirectory) || !(0, fs_1.existsSync)(skillFilePath)) {
+ return undefined;
+ }
+ return { skillDirectory, skillFilePath };
+};
+const validateSkillFile = (allModules, manifestDir, yamlContentByLine) => {
+ const validationErrors = [];
+ const rovoSkills = allModules[types_1.AllModuleTypes.RovoSkill];
+ rovoSkills?.forEach((skill) => {
+ const skillPath = resolveSkillFilePath(skill, manifestDir);
+ if (!skillPath) {
+ return;
+ }
+ const { skillDirectory, skillFilePath } = skillPath;
+ const skillContent = (0, fs_1.readFileSync)(skillFilePath, 'utf8');
+ const normalizedSkillContent = skillContent.replace(/^\uFEFF/, '');
+ const lines = normalizedSkillContent.split(/\r?\n/);
+ if (lines[0] !== '---') {
+ const errorContext = {
+ skill,
+ skillDirectory,
+ parentDirectoryName: (0, path_1.basename)(skillDirectory),
+ frontmatter: {},
+ body: '',
+ bodyLines: [],
+ referencedBodyFiles: [],
+ dependencyTools: [],
+ unknownFrontmatterFields: [],
+ metadata: undefined,
+ metadataEntries: [],
+ allowedTools: undefined
+ };
+ validationErrors.push(...collectRuleErrors(errorContext, [
+ (0, skill_file_rules_1.createAlwaysFailValidator)('error', () => text_1.errors.modules.rovo.skillFileMissingFrontmatterDelimiters(skill.key))
+ ], yamlContentByLine));
+ return;
+ }
+ const frontmatterEndIndex = lines.findIndex((line, index) => index > 0 && line === '---');
+ if (frontmatterEndIndex === -1) {
+ const errorContext = {
+ skill,
+ skillDirectory,
+ parentDirectoryName: (0, path_1.basename)(skillDirectory),
+ frontmatter: {},
+ body: '',
+ bodyLines: [],
+ referencedBodyFiles: [],
+ dependencyTools: [],
+ unknownFrontmatterFields: [],
+ metadata: undefined,
+ metadataEntries: [],
+ allowedTools: undefined
+ };
+ validationErrors.push(...collectRuleErrors(errorContext, [
+ (0, skill_file_rules_1.createAlwaysFailValidator)('error', () => text_1.errors.modules.rovo.skillFileMissingFrontmatterDelimiters(skill.key))
+ ], yamlContentByLine));
+ return;
+ }
+ const frontmatterContent = lines.slice(1, frontmatterEndIndex).join('\n');
+ const body = lines.slice(frontmatterEndIndex + 1).join('\n');
+ const bodyLines = body.split(/\r?\n/);
+ const referencedBodyFiles = extractReferencedBodyFiles(body);
+ let parsed;
+ try {
+ parsed = yaml_1.default.parse(frontmatterContent);
+ }
+ catch {
+ const errorContext = {
+ skill,
+ skillDirectory,
+ parentDirectoryName: (0, path_1.basename)(skillDirectory),
+ frontmatter: {},
+ body,
+ bodyLines,
+ referencedBodyFiles,
+ dependencyTools: [],
+ unknownFrontmatterFields: [],
+ metadata: undefined,
+ metadataEntries: [],
+ allowedTools: undefined
+ };
+ validationErrors.push(...collectRuleErrors(errorContext, [(0, skill_file_rules_1.createAlwaysFailValidator)('error', () => text_1.errors.modules.rovo.skillFileFrontmatterYamlInvalid(skill.key))], yamlContentByLine));
+ return;
+ }
+ if (!(0, skill_file_rules_1.isRecord)(parsed)) {
+ const errorContext = {
+ skill,
+ skillDirectory,
+ parentDirectoryName: (0, path_1.basename)(skillDirectory),
+ frontmatter: {},
+ body,
+ bodyLines,
+ referencedBodyFiles,
+ dependencyTools: [],
+ unknownFrontmatterFields: [],
+ metadata: undefined,
+ metadataEntries: [],
+ allowedTools: undefined
+ };
+ validationErrors.push(...collectRuleErrors(errorContext, [(0, skill_file_rules_1.createAlwaysFailValidator)('error', () => text_1.errors.modules.rovo.skillFileFrontmatterYamlInvalid(skill.key))], yamlContentByLine));
+ return;
+ }
+ const declaredToolDependencies = skill.dependencies?.tools;
+ const dependencyTools = Array.isArray(declaredToolDependencies)
+ ? declaredToolDependencies.filter((tool) => typeof tool === 'string')
+ : [];
+ const context = {
+ skill,
+ skillDirectory,
+ parentDirectoryName: (0, path_1.basename)(skillDirectory),
+ frontmatter: parsed,
+ body,
+ bodyLines,
+ referencedBodyFiles,
+ dependencyTools,
+ unknownFrontmatterFields: Object.keys(parsed).filter((field) => !skill_file_rules_1.allowedFrontmatterFields.has(field)),
+ metadata: parsed.metadata,
+ metadataEntries: (0, skill_file_rules_1.isRecord)(parsed.metadata) ? Object.entries(parsed.metadata) : [],
+ allowedTools: parsed['allowed-tools']
+ };
+ const unknownFieldValidators = (0, skill_file_rules_1.buildUnknownFrontmatterFieldValidators)(context);
+ const metadataValueValidators = (0, skill_file_rules_1.buildMetadataValueValidators)(context);
+ const referencedFileValidators = (0, skill_file_rules_1.buildReferencedFileValidators)(context);
+ const validators = [
+ ...unknownFieldValidators,
+ ...skill_file_rules_1.skillValidationRules,
+ ...metadataValueValidators,
+ ...referencedFileValidators
+ ];
+ validationErrors.push(...collectRuleErrors(context, validators, yamlContentByLine));
+ });
+ return validationErrors;
+};
+exports.validateSkillFile = validateSkillFile;