Initial commit with translated description
This commit is contained in:
170
skills_monitor.js
Normal file
170
skills_monitor.js
Normal file
@@ -0,0 +1,170 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// SKILLS MONITOR (v2.0)
|
||||
// Proactively checks installed skills for real issues (not cosmetic ones).
|
||||
// - Ignores shared libraries and non-skill directories
|
||||
// - Only syntax-checks .js files
|
||||
// - Checks if dependencies are truly missing (not just node_modules dir)
|
||||
|
||||
const SKILLS_DIR = path.resolve(__dirname, '../../skills');
|
||||
|
||||
// Directories that are NOT skills (shared libs, internal tools, non-JS projects)
|
||||
const IGNORE_LIST = new Set([
|
||||
'common', // Shared Feishu client library
|
||||
'clawhub', // ClawHub CLI integration
|
||||
'input-validator', // Internal validation utility
|
||||
'proactive-agent', // Agent framework (not a skill)
|
||||
'security-audit', // Internal audit tool
|
||||
]);
|
||||
|
||||
// Load user-defined ignore list if exists
|
||||
try {
|
||||
const ignoreFile = path.join(SKILLS_DIR, '..', '.skill_monitor_ignore');
|
||||
if (fs.existsSync(ignoreFile)) {
|
||||
const lines = fs.readFileSync(ignoreFile, 'utf8').split('\n');
|
||||
lines.forEach(function(l) {
|
||||
var t = l.trim();
|
||||
if (t && !t.startsWith('#')) IGNORE_LIST.add(t);
|
||||
});
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
function checkSkill(skillName) {
|
||||
if (IGNORE_LIST.has(skillName)) return null;
|
||||
|
||||
const skillPath = path.join(SKILLS_DIR, skillName);
|
||||
const issues = [];
|
||||
|
||||
// Skip if not a directory
|
||||
try {
|
||||
if (!fs.statSync(skillPath).isDirectory()) return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. Check Package Structure
|
||||
let mainFile = 'index.js';
|
||||
const pkgPath = path.join(skillPath, 'package.json');
|
||||
var hasPkg = false;
|
||||
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
hasPkg = true;
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
if (pkg.main) mainFile = pkg.main;
|
||||
|
||||
// 2. Check dependencies -- only flag if require() actually fails
|
||||
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
|
||||
if (!fs.existsSync(path.join(skillPath, 'node_modules'))) {
|
||||
// Try to actually require the entry point to see if it works without node_modules
|
||||
var entryAbs = path.join(skillPath, mainFile);
|
||||
if (fs.existsSync(entryAbs) && mainFile.endsWith('.js')) {
|
||||
try {
|
||||
execSync(`node -e "require('${entryAbs.replace(/'/g, "\\'")}')"`, {
|
||||
stdio: 'ignore', timeout: 5000, cwd: skillPath, windowsHide: true
|
||||
});
|
||||
// require succeeded: deps are resolved via relative paths or globals, no issue
|
||||
} catch (e) {
|
||||
issues.push('Missing node_modules (needs npm install)');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
issues.push('Invalid package.json');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Syntax Check -- only for .js entry points
|
||||
if (mainFile.endsWith('.js')) {
|
||||
const entryPoint = path.join(skillPath, mainFile);
|
||||
if (fs.existsSync(entryPoint)) {
|
||||
try {
|
||||
execSync(`node -c "${entryPoint}"`, { stdio: 'ignore', timeout: 5000, windowsHide: true });
|
||||
} catch (e) {
|
||||
issues.push(`Syntax Error in ${mainFile}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Missing SKILL.md -- only warn for dirs that have package.json (real skills, not utility dirs)
|
||||
if (hasPkg && !fs.existsSync(path.join(skillPath, 'SKILL.md'))) {
|
||||
issues.push('Missing SKILL.md');
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
return { name: skillName, issues };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Auto-heal: attempt to fix simple issues automatically
|
||||
function autoHeal(skillName, issues) {
|
||||
const skillPath = path.join(SKILLS_DIR, skillName);
|
||||
const healed = [];
|
||||
|
||||
for (const issue of issues) {
|
||||
if (issue === 'Missing node_modules (needs npm install)') {
|
||||
try {
|
||||
execSync('npm install --production --no-audit --no-fund', {
|
||||
cwd: skillPath, stdio: 'ignore', timeout: 30000, windowsHide: true
|
||||
});
|
||||
healed.push(issue);
|
||||
console.log(`[SkillsMonitor] Auto-healed ${skillName}: npm install`);
|
||||
} catch (e) {
|
||||
// npm install failed, leave the issue
|
||||
}
|
||||
} else if (issue === 'Missing SKILL.md') {
|
||||
try {
|
||||
const name = skillName.replace(/-/g, ' ');
|
||||
fs.writeFileSync(
|
||||
path.join(skillPath, 'SKILL.md'),
|
||||
`# ${skillName}\n\n${name} skill.\n`
|
||||
);
|
||||
healed.push(issue);
|
||||
console.log(`[SkillsMonitor] Auto-healed ${skillName}: created SKILL.md stub`);
|
||||
} catch (e) {
|
||||
// write failed, leave the issue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return healed;
|
||||
}
|
||||
|
||||
function run(options) {
|
||||
const heal = (options && options.autoHeal) !== false; // auto-heal by default
|
||||
const skills = fs.readdirSync(SKILLS_DIR);
|
||||
const report = [];
|
||||
|
||||
for (const skill of skills) {
|
||||
if (skill.startsWith('.')) continue; // skip hidden
|
||||
const result = checkSkill(skill);
|
||||
if (result) {
|
||||
if (heal) {
|
||||
const healed = autoHeal(result.name, result.issues);
|
||||
// Remove healed issues
|
||||
result.issues = result.issues.filter(function(i) { return !healed.includes(i); });
|
||||
if (result.issues.length === 0) continue; // fully healed
|
||||
}
|
||||
report.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const issues = run();
|
||||
if (issues.length > 0) {
|
||||
console.log(JSON.stringify(issues, null, 2));
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("[]");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
Reference in New Issue
Block a user