feat(skills): validate manifest installer specs

This commit is contained in:
William Valentin
2026-02-12 17:52:53 -08:00
parent bd29afeaff
commit 81d04357a1
5 changed files with 177 additions and 5 deletions
+46
View File
@@ -11,6 +11,48 @@ import { execSync } from 'child_process';
import { platform } from 'os';
import type { Skill, SkillManifest, SkillRequirements, SkillTier } from './types.js';
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
function hasValidInstallers(manifest: unknown): boolean {
if (!manifest || typeof manifest !== 'object') {
return false;
}
const candidate = manifest as { installers?: unknown };
if (candidate.installers === undefined) {
return true;
}
if (!Array.isArray(candidate.installers)) {
return false;
}
return candidate.installers.every((installer) => {
if (!installer || typeof installer !== 'object') {
return false;
}
const typedInstaller = installer as { type?: unknown; packages?: unknown; url?: unknown; destination?: unknown };
if (typedInstaller.type === 'brew' || typedInstaller.type === 'node' || typedInstaller.type === 'go') {
return isStringArray(typedInstaller.packages);
}
if (typedInstaller.type === 'download') {
if (typeof typedInstaller.url !== 'string') {
return false;
}
if (typedInstaller.destination !== undefined && typeof typedInstaller.destination !== 'string') {
return false;
}
return true;
}
return false;
});
}
/**
* Check whether a skill's system requirements are met.
*
@@ -90,6 +132,10 @@ export function loadSkill(directory: string, tier: SkillTier): Skill | null {
console.warn(`Skill manifest at ${manifestPath} missing required fields (name, description, version)`);
return null;
}
if (!hasValidInstallers(raw)) {
console.warn(`Skill manifest at ${manifestPath} has invalid installers specification`);
return null;
}
manifest = {
...raw,