mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-31 06:03:29 +08:00
212 lines
6.7 KiB
JavaScript
212 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Validate selective-install manifests and profile/module relationships.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const Ajv = require('ajv');
|
|
|
|
const REPO_ROOT = path.join(__dirname, '../..');
|
|
const MODULES_MANIFEST_PATH = path.join(REPO_ROOT, 'manifests/install-modules.json');
|
|
const PROFILES_MANIFEST_PATH = path.join(REPO_ROOT, 'manifests/install-profiles.json');
|
|
const COMPONENTS_MANIFEST_PATH = path.join(REPO_ROOT, 'manifests/install-components.json');
|
|
const MODULES_SCHEMA_PATH = path.join(REPO_ROOT, 'schemas/install-modules.schema.json');
|
|
const PROFILES_SCHEMA_PATH = path.join(REPO_ROOT, 'schemas/install-profiles.schema.json');
|
|
const COMPONENTS_SCHEMA_PATH = path.join(REPO_ROOT, 'schemas/install-components.schema.json');
|
|
const COMPONENT_FAMILY_PREFIXES = {
|
|
baseline: 'baseline:',
|
|
language: 'lang:',
|
|
framework: 'framework:',
|
|
capability: 'capability:',
|
|
};
|
|
|
|
function readJson(filePath, label) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
} catch (error) {
|
|
throw new Error(`Invalid JSON in ${label}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function normalizeRelativePath(relativePath) {
|
|
return String(relativePath).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
}
|
|
|
|
function validateSchema(ajv, schemaPath, data, label) {
|
|
const schema = readJson(schemaPath, `${label} schema`);
|
|
const validate = ajv.compile(schema);
|
|
const valid = validate(data);
|
|
|
|
if (!valid) {
|
|
for (const error of validate.errors) {
|
|
console.error(
|
|
`ERROR: ${label} schema: ${error.instancePath || '/'} ${error.message}`
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function validateInstallManifests() {
|
|
if (!fs.existsSync(MODULES_MANIFEST_PATH) || !fs.existsSync(PROFILES_MANIFEST_PATH)) {
|
|
console.log('Install manifests not found, skipping validation');
|
|
process.exit(0);
|
|
}
|
|
|
|
let hasErrors = false;
|
|
let modulesData;
|
|
let profilesData;
|
|
let componentsData = { version: null, components: [] };
|
|
|
|
try {
|
|
modulesData = readJson(MODULES_MANIFEST_PATH, 'install-modules.json');
|
|
profilesData = readJson(PROFILES_MANIFEST_PATH, 'install-profiles.json');
|
|
if (fs.existsSync(COMPONENTS_MANIFEST_PATH)) {
|
|
componentsData = readJson(COMPONENTS_MANIFEST_PATH, 'install-components.json');
|
|
}
|
|
} catch (error) {
|
|
console.error(`ERROR: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const ajv = new Ajv({ allErrors: true });
|
|
hasErrors = validateSchema(ajv, MODULES_SCHEMA_PATH, modulesData, 'install-modules.json') || hasErrors;
|
|
hasErrors = validateSchema(ajv, PROFILES_SCHEMA_PATH, profilesData, 'install-profiles.json') || hasErrors;
|
|
if (fs.existsSync(COMPONENTS_MANIFEST_PATH)) {
|
|
hasErrors = validateSchema(ajv, COMPONENTS_SCHEMA_PATH, componentsData, 'install-components.json') || hasErrors;
|
|
}
|
|
|
|
if (hasErrors) {
|
|
process.exit(1);
|
|
}
|
|
|
|
const modules = Array.isArray(modulesData.modules) ? modulesData.modules : [];
|
|
const moduleIds = new Set();
|
|
const claimedPaths = new Map();
|
|
|
|
for (const module of modules) {
|
|
if (moduleIds.has(module.id)) {
|
|
console.error(`ERROR: Duplicate install module id: ${module.id}`);
|
|
hasErrors = true;
|
|
}
|
|
moduleIds.add(module.id);
|
|
|
|
for (const dependency of module.dependencies) {
|
|
if (!moduleIds.has(dependency) && !modules.some(candidate => candidate.id === dependency)) {
|
|
console.error(`ERROR: Module ${module.id} depends on unknown module ${dependency}`);
|
|
hasErrors = true;
|
|
}
|
|
if (dependency === module.id) {
|
|
console.error(`ERROR: Module ${module.id} cannot depend on itself`);
|
|
hasErrors = true;
|
|
}
|
|
}
|
|
|
|
for (const relativePath of module.paths) {
|
|
const normalizedPath = normalizeRelativePath(relativePath);
|
|
const absolutePath = path.join(REPO_ROOT, normalizedPath);
|
|
|
|
if (!fs.existsSync(absolutePath)) {
|
|
console.error(
|
|
`ERROR: Module ${module.id} references missing path: ${normalizedPath}`
|
|
);
|
|
hasErrors = true;
|
|
}
|
|
|
|
if (claimedPaths.has(normalizedPath)) {
|
|
console.error(
|
|
`ERROR: Install path ${normalizedPath} is claimed by both ${claimedPaths.get(normalizedPath)} and ${module.id}`
|
|
);
|
|
hasErrors = true;
|
|
} else {
|
|
claimedPaths.set(normalizedPath, module.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
const profiles = profilesData.profiles || {};
|
|
const components = Array.isArray(componentsData.components) ? componentsData.components : [];
|
|
const expectedProfileIds = ['core', 'developer', 'security', 'research', 'full'];
|
|
|
|
for (const profileId of expectedProfileIds) {
|
|
if (!profiles[profileId]) {
|
|
console.error(`ERROR: Missing required install profile: ${profileId}`);
|
|
hasErrors = true;
|
|
}
|
|
}
|
|
|
|
for (const [profileId, profile] of Object.entries(profiles)) {
|
|
const seenModules = new Set();
|
|
for (const moduleId of profile.modules) {
|
|
if (!moduleIds.has(moduleId)) {
|
|
console.error(
|
|
`ERROR: Profile ${profileId} references unknown module ${moduleId}`
|
|
);
|
|
hasErrors = true;
|
|
}
|
|
|
|
if (seenModules.has(moduleId)) {
|
|
console.error(
|
|
`ERROR: Profile ${profileId} contains duplicate module ${moduleId}`
|
|
);
|
|
hasErrors = true;
|
|
}
|
|
seenModules.add(moduleId);
|
|
}
|
|
}
|
|
|
|
if (profiles.full) {
|
|
const fullModules = new Set(profiles.full.modules);
|
|
for (const moduleId of moduleIds) {
|
|
if (!fullModules.has(moduleId)) {
|
|
console.error(`ERROR: full profile is missing module ${moduleId}`);
|
|
hasErrors = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const componentIds = new Set();
|
|
for (const component of components) {
|
|
if (componentIds.has(component.id)) {
|
|
console.error(`ERROR: Duplicate install component id: ${component.id}`);
|
|
hasErrors = true;
|
|
}
|
|
componentIds.add(component.id);
|
|
|
|
const expectedPrefix = COMPONENT_FAMILY_PREFIXES[component.family];
|
|
if (expectedPrefix && !component.id.startsWith(expectedPrefix)) {
|
|
console.error(
|
|
`ERROR: Component ${component.id} does not match expected ${component.family} prefix ${expectedPrefix}`
|
|
);
|
|
hasErrors = true;
|
|
}
|
|
|
|
const seenModules = new Set();
|
|
for (const moduleId of component.modules) {
|
|
if (!moduleIds.has(moduleId)) {
|
|
console.error(`ERROR: Component ${component.id} references unknown module ${moduleId}`);
|
|
hasErrors = true;
|
|
}
|
|
|
|
if (seenModules.has(moduleId)) {
|
|
console.error(`ERROR: Component ${component.id} contains duplicate module ${moduleId}`);
|
|
hasErrors = true;
|
|
}
|
|
seenModules.add(moduleId);
|
|
}
|
|
}
|
|
|
|
if (hasErrors) {
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
`Validated ${modules.length} install modules, ${components.length} install components, and ${Object.keys(profiles).length} profiles`
|
|
);
|
|
}
|
|
|
|
validateInstallManifests();
|