@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;