Files
Profa 49cd27b57a fix(platform): cross-platform compatibility for Windows/macOS/Linux
Replace all bash-specific syntax in npm scripts, lifecycle hooks, and
TypeScript execSync calls with cross-platform Node.js equivalents so
aqe works on Windows (cmd.exe/PowerShell) without WSL.

CRITICAL fixes:
- package.json: replace mkdir -p, echo >, rm -rf with node -e inline
- v3/package.json: replace bash scripts with .cjs Node.js scripts
- New prepare-assets.cjs, sync-agents.cjs, sync-agents-check.cjs

HIGH fixes:
- Remove 2>/dev/null from 7 TypeScript execSync calls (try/catch suffices)
- Add process.env.USERPROFILE fallback for Windows HOME detection
- Simplify hook commands: remove [ -n ], || true, $(date +%s) bash-isms
- preinstall.cjs: cross-platform binary detection (which/where)

Skill validation framework:
- New validator-lib.cjs + validate-skill.cjs universal validator
- migrate-validators.cjs auto-extracts 107 validate-config.json files
- Replaces 52 individual validate.sh scripts with single Node.js framework

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 09:14:14 +00:00

172 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
/**
* Migrate validate.sh → validate-config.json
*
* Auto-extracts configuration from existing bash validate.sh scripts
* and generates cross-platform validate-config.json files.
*
* Usage:
* node scripts/migrate-validators.cjs # migrate all skills
* node scripts/migrate-validators.cjs --dry-run # preview without writing
* node scripts/migrate-validators.cjs --skill api-testing-patterns # single skill
*/
const fs = require('fs');
const path = require('path');
const RESET = '\x1b[0m';
const CYAN = '\x1b[36m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const RED = '\x1b[31m';
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const singleSkill = args.includes('--skill') ? args[args.indexOf('--skill') + 1] : null;
const PROJECT_ROOT = path.resolve(__dirname, '..');
const SKILLS_DIRS = [
path.join(PROJECT_ROOT, '.claude', 'skills'),
path.join(PROJECT_ROOT, 'v3', '.claude', 'skills'),
];
function extractConfig(shContent, skillDir) {
const config = {
skillName: '',
skillVersion: '1.0.0',
requiredTools: [],
optionalTools: [],
schemaPath: null,
requiredFields: [],
requiredNonEmptyFields: [],
mustContainTerms: [],
mustNotContainTerms: [],
enumValidations: {},
};
// Extract SKILL_NAME
const nameMatch = shContent.match(/SKILL_NAME="([^"]+)"/);
if (nameMatch) config.skillName = nameMatch[1];
// Extract SKILL_VERSION
const versionMatch = shContent.match(/SKILL_VERSION="([^"]+)"/);
if (versionMatch) config.skillVersion = versionMatch[1];
// Extract REQUIRED_TOOLS
const reqToolsMatch = shContent.match(/REQUIRED_TOOLS=\(([^)]*)\)/);
if (reqToolsMatch) {
config.requiredTools = reqToolsMatch[1].match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
}
// Extract OPTIONAL_TOOLS
const optToolsMatch = shContent.match(/OPTIONAL_TOOLS=\(([^)]*)\)/);
if (optToolsMatch) {
config.optionalTools = optToolsMatch[1].match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
}
// Extract SCHEMA_PATH (relative)
const schemaMatch = shContent.match(/SCHEMA_PATH="\$SKILL_DIR\/([^"]+)"/);
if (schemaMatch) config.schemaPath = schemaMatch[1];
// Extract REQUIRED_FIELDS
const reqFieldsMatch = shContent.match(/REQUIRED_FIELDS=\(([^)]*)\)/);
if (reqFieldsMatch) {
config.requiredFields = reqFieldsMatch[1].match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
}
// Extract REQUIRED_NON_EMPTY_FIELDS
const reqNonEmptyMatch = shContent.match(/REQUIRED_NON_EMPTY_FIELDS=\(([^)]*)\)/);
if (reqNonEmptyMatch) {
config.requiredNonEmptyFields = reqNonEmptyMatch[1].match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
}
// Extract MUST_CONTAIN_TERMS
const mustContainMatch = shContent.match(/MUST_CONTAIN_TERMS=\(([^)]*)\)/);
if (mustContainMatch) {
config.mustContainTerms = mustContainMatch[1].match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
}
// Extract MUST_NOT_CONTAIN_TERMS
const mustNotContainMatch = shContent.match(/MUST_NOT_CONTAIN_TERMS=\(([^)]*)\)/);
if (mustNotContainMatch) {
config.mustNotContainTerms = mustNotContainMatch[1].match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
}
// Extract ENUM_VALIDATIONS
const enumMatch = shContent.match(/ENUM_VALIDATIONS=\(([\s\S]*?)\)/);
if (enumMatch) {
const enumLines = enumMatch[1].match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || [];
for (const line of enumLines) {
const [fieldPath, values] = line.split(':');
if (fieldPath && values) {
config.enumValidations[fieldPath] = values.split(',');
}
}
}
return config;
}
let migrated = 0;
let skipped = 0;
let failed = 0;
for (const skillsDir of SKILLS_DIRS) {
if (!fs.existsSync(skillsDir)) continue;
const skills = fs.readdirSync(skillsDir).filter(d => {
if (d.startsWith('.') || d.endsWith('.json') || d.endsWith('.md')) return false;
if (singleSkill && d !== singleSkill) return false;
return fs.statSync(path.join(skillsDir, d)).isDirectory();
});
for (const skill of skills) {
const shPath = path.join(skillsDir, skill, 'scripts', 'validate.sh');
const configPath = path.join(skillsDir, skill, 'scripts', 'validate-config.json');
if (!fs.existsSync(shPath)) {
skipped++;
continue;
}
// Skip if config already exists and not in single-skill mode
if (fs.existsSync(configPath) && !singleSkill) {
console.log(`${YELLOW}[SKIP]${RESET} ${skill} — validate-config.json already exists`);
skipped++;
continue;
}
try {
const shContent = fs.readFileSync(shPath, 'utf-8');
const config = extractConfig(shContent, path.join(skillsDir, skill));
// Use skill directory name if SKILL_NAME not found
if (!config.skillName) config.skillName = skill;
if (dryRun) {
console.log(`${CYAN}[DRY-RUN]${RESET} ${skill}:`);
console.log(JSON.stringify(config, null, 2));
console.log('');
} else {
// Ensure scripts directory exists
const scriptsDir = path.join(skillsDir, skill, 'scripts');
if (!fs.existsSync(scriptsDir)) fs.mkdirSync(scriptsDir, { recursive: true });
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
console.log(`${GREEN}[OK]${RESET} ${skill} → validate-config.json`);
}
migrated++;
} catch (e) {
console.log(`${RED}[FAIL]${RESET} ${skill}: ${e.message}`);
failed++;
}
}
}
console.log('');
console.log('==============================================');
console.log(`Migration complete: ${migrated} migrated, ${skipped} skipped, ${failed} failed`);
if (dryRun) console.log('(dry-run mode — no files were written)');
console.log('==============================================');