mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Add agent: and skill: component families to the install component catalog, enabling fine-grained selective install via CLI flags: ecc install --profile developer --with lang:typescript --without capability:orchestration ecc install --with lang:python --with agent:security-reviewer Changes: - Add agent: family (9 entries) and skill: family (10 entries) to manifests/install-components.json for granular component addressing - Update install-components.schema.json to accept agent: and skill: family prefixes - Register agent and skill family prefixes in COMPONENT_FAMILY_PREFIXES (scripts/lib/install-manifests.js) - Add 41 comprehensive tests covering CLI parsing, request normalization, component catalog validation, plan resolution, target filtering, error handling, and end-to-end install with --with/--without flags Closes #470
443 lines
14 KiB
JavaScript
443 lines
14 KiB
JavaScript
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { planInstallTargetScaffold } = require('./install-targets/registry');
|
|
|
|
const DEFAULT_REPO_ROOT = path.join(__dirname, '../..');
|
|
const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'opencode'];
|
|
const COMPONENT_FAMILY_PREFIXES = {
|
|
baseline: 'baseline:',
|
|
language: 'lang:',
|
|
framework: 'framework:',
|
|
capability: 'capability:',
|
|
agent: 'agent:',
|
|
skill: 'skill:',
|
|
};
|
|
const LEGACY_COMPAT_BASE_MODULE_IDS_BY_TARGET = Object.freeze({
|
|
claude: [
|
|
'rules-core',
|
|
'agents-core',
|
|
'commands-core',
|
|
'hooks-runtime',
|
|
'platform-configs',
|
|
'workflow-quality',
|
|
],
|
|
cursor: [
|
|
'rules-core',
|
|
'agents-core',
|
|
'commands-core',
|
|
'hooks-runtime',
|
|
'platform-configs',
|
|
'workflow-quality',
|
|
],
|
|
antigravity: [
|
|
'rules-core',
|
|
'agents-core',
|
|
'commands-core',
|
|
],
|
|
});
|
|
const LEGACY_LANGUAGE_ALIAS_TO_CANONICAL = Object.freeze({
|
|
go: 'go',
|
|
golang: 'go',
|
|
java: 'java',
|
|
javascript: 'typescript',
|
|
kotlin: 'java',
|
|
perl: 'perl',
|
|
php: 'php',
|
|
python: 'python',
|
|
swift: 'swift',
|
|
typescript: 'typescript',
|
|
});
|
|
const LEGACY_LANGUAGE_EXTRA_MODULE_IDS = Object.freeze({
|
|
go: ['framework-language'],
|
|
java: ['framework-language'],
|
|
perl: [],
|
|
php: [],
|
|
python: ['framework-language'],
|
|
swift: [],
|
|
typescript: ['framework-language'],
|
|
});
|
|
|
|
function readJson(filePath, label) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
} catch (error) {
|
|
throw new Error(`Failed to read ${label}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function dedupeStrings(values) {
|
|
return [...new Set((Array.isArray(values) ? values : []).map(value => String(value).trim()).filter(Boolean))];
|
|
}
|
|
|
|
function assertKnownModuleIds(moduleIds, manifests) {
|
|
const unknownModuleIds = dedupeStrings(moduleIds)
|
|
.filter(moduleId => !manifests.modulesById.has(moduleId));
|
|
|
|
if (unknownModuleIds.length === 1) {
|
|
throw new Error(`Unknown install module: ${unknownModuleIds[0]}`);
|
|
}
|
|
|
|
if (unknownModuleIds.length > 1) {
|
|
throw new Error(`Unknown install modules: ${unknownModuleIds.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
function intersectTargets(modules) {
|
|
if (!Array.isArray(modules) || modules.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
return SUPPORTED_INSTALL_TARGETS.filter(target => (
|
|
modules.every(module => Array.isArray(module.targets) && module.targets.includes(target))
|
|
));
|
|
}
|
|
|
|
function getManifestPaths(repoRoot = DEFAULT_REPO_ROOT) {
|
|
return {
|
|
modulesPath: path.join(repoRoot, 'manifests', 'install-modules.json'),
|
|
profilesPath: path.join(repoRoot, 'manifests', 'install-profiles.json'),
|
|
componentsPath: path.join(repoRoot, 'manifests', 'install-components.json'),
|
|
};
|
|
}
|
|
|
|
function loadInstallManifests(options = {}) {
|
|
const repoRoot = options.repoRoot || DEFAULT_REPO_ROOT;
|
|
const { modulesPath, profilesPath, componentsPath } = getManifestPaths(repoRoot);
|
|
|
|
if (!fs.existsSync(modulesPath) || !fs.existsSync(profilesPath)) {
|
|
throw new Error(`Install manifests not found under ${repoRoot}`);
|
|
}
|
|
|
|
const modulesData = readJson(modulesPath, 'install-modules.json');
|
|
const profilesData = readJson(profilesPath, 'install-profiles.json');
|
|
const componentsData = fs.existsSync(componentsPath)
|
|
? readJson(componentsPath, 'install-components.json')
|
|
: { version: null, components: [] };
|
|
const modules = Array.isArray(modulesData.modules) ? modulesData.modules : [];
|
|
const profiles = profilesData && typeof profilesData.profiles === 'object'
|
|
? profilesData.profiles
|
|
: {};
|
|
const components = Array.isArray(componentsData.components) ? componentsData.components : [];
|
|
const modulesById = new Map(modules.map(module => [module.id, module]));
|
|
const componentsById = new Map(components.map(component => [component.id, component]));
|
|
|
|
return {
|
|
repoRoot,
|
|
modulesPath,
|
|
profilesPath,
|
|
componentsPath,
|
|
modules,
|
|
profiles,
|
|
components,
|
|
modulesById,
|
|
componentsById,
|
|
modulesVersion: modulesData.version,
|
|
profilesVersion: profilesData.version,
|
|
componentsVersion: componentsData.version,
|
|
};
|
|
}
|
|
|
|
function listInstallProfiles(options = {}) {
|
|
const manifests = loadInstallManifests(options);
|
|
return Object.entries(manifests.profiles).map(([id, profile]) => ({
|
|
id,
|
|
description: profile.description,
|
|
moduleCount: Array.isArray(profile.modules) ? profile.modules.length : 0,
|
|
}));
|
|
}
|
|
|
|
function listInstallModules(options = {}) {
|
|
const manifests = loadInstallManifests(options);
|
|
return manifests.modules.map(module => ({
|
|
id: module.id,
|
|
kind: module.kind,
|
|
description: module.description,
|
|
targets: module.targets,
|
|
defaultInstall: module.defaultInstall,
|
|
cost: module.cost,
|
|
stability: module.stability,
|
|
dependencyCount: Array.isArray(module.dependencies) ? module.dependencies.length : 0,
|
|
}));
|
|
}
|
|
|
|
function listLegacyCompatibilityLanguages() {
|
|
return Object.keys(LEGACY_LANGUAGE_ALIAS_TO_CANONICAL).sort();
|
|
}
|
|
|
|
function validateInstallModuleIds(moduleIds, options = {}) {
|
|
const manifests = loadInstallManifests(options);
|
|
const normalizedModuleIds = dedupeStrings(moduleIds);
|
|
assertKnownModuleIds(normalizedModuleIds, manifests);
|
|
return normalizedModuleIds;
|
|
}
|
|
|
|
function listInstallComponents(options = {}) {
|
|
const manifests = loadInstallManifests(options);
|
|
const family = options.family || null;
|
|
const target = options.target || null;
|
|
|
|
if (family && !Object.hasOwn(COMPONENT_FAMILY_PREFIXES, family)) {
|
|
throw new Error(
|
|
`Unknown component family: ${family}. Expected one of ${Object.keys(COMPONENT_FAMILY_PREFIXES).join(', ')}`
|
|
);
|
|
}
|
|
|
|
if (target && !SUPPORTED_INSTALL_TARGETS.includes(target)) {
|
|
throw new Error(
|
|
`Unknown install target: ${target}. Expected one of ${SUPPORTED_INSTALL_TARGETS.join(', ')}`
|
|
);
|
|
}
|
|
|
|
return manifests.components
|
|
.filter(component => !family || component.family === family)
|
|
.map(component => {
|
|
const moduleIds = dedupeStrings(component.modules);
|
|
const modules = moduleIds
|
|
.map(moduleId => manifests.modulesById.get(moduleId))
|
|
.filter(Boolean);
|
|
const targets = intersectTargets(modules);
|
|
|
|
return {
|
|
id: component.id,
|
|
family: component.family,
|
|
description: component.description,
|
|
moduleIds,
|
|
moduleCount: moduleIds.length,
|
|
targets,
|
|
};
|
|
})
|
|
.filter(component => !target || component.targets.includes(target));
|
|
}
|
|
|
|
function expandComponentIdsToModuleIds(componentIds, manifests) {
|
|
const expandedModuleIds = [];
|
|
|
|
for (const componentId of dedupeStrings(componentIds)) {
|
|
const component = manifests.componentsById.get(componentId);
|
|
if (!component) {
|
|
throw new Error(`Unknown install component: ${componentId}`);
|
|
}
|
|
expandedModuleIds.push(...component.modules);
|
|
}
|
|
|
|
return dedupeStrings(expandedModuleIds);
|
|
}
|
|
|
|
function resolveLegacyCompatibilitySelection(options = {}) {
|
|
const manifests = loadInstallManifests(options);
|
|
const target = options.target || null;
|
|
|
|
if (target && !SUPPORTED_INSTALL_TARGETS.includes(target)) {
|
|
throw new Error(
|
|
`Unknown install target: ${target}. Expected one of ${SUPPORTED_INSTALL_TARGETS.join(', ')}`
|
|
);
|
|
}
|
|
|
|
const legacyLanguages = dedupeStrings(options.legacyLanguages)
|
|
.map(language => language.toLowerCase());
|
|
const normalizedLegacyLanguages = dedupeStrings(legacyLanguages);
|
|
|
|
if (normalizedLegacyLanguages.length === 0) {
|
|
throw new Error('No legacy languages were provided');
|
|
}
|
|
|
|
const unknownLegacyLanguages = normalizedLegacyLanguages
|
|
.filter(language => !Object.hasOwn(LEGACY_LANGUAGE_ALIAS_TO_CANONICAL, language));
|
|
|
|
if (unknownLegacyLanguages.length === 1) {
|
|
throw new Error(
|
|
`Unknown legacy language: ${unknownLegacyLanguages[0]}. Expected one of ${listLegacyCompatibilityLanguages().join(', ')}`
|
|
);
|
|
}
|
|
|
|
if (unknownLegacyLanguages.length > 1) {
|
|
throw new Error(
|
|
`Unknown legacy languages: ${unknownLegacyLanguages.join(', ')}. Expected one of ${listLegacyCompatibilityLanguages().join(', ')}`
|
|
);
|
|
}
|
|
|
|
const canonicalLegacyLanguages = normalizedLegacyLanguages
|
|
.map(language => LEGACY_LANGUAGE_ALIAS_TO_CANONICAL[language]);
|
|
const baseModuleIds = LEGACY_COMPAT_BASE_MODULE_IDS_BY_TARGET[target || 'claude']
|
|
|| LEGACY_COMPAT_BASE_MODULE_IDS_BY_TARGET.claude;
|
|
const moduleIds = dedupeStrings([
|
|
...baseModuleIds,
|
|
...(target === 'antigravity'
|
|
? []
|
|
: canonicalLegacyLanguages.flatMap(language => LEGACY_LANGUAGE_EXTRA_MODULE_IDS[language] || [])),
|
|
]);
|
|
|
|
assertKnownModuleIds(moduleIds, manifests);
|
|
|
|
return {
|
|
legacyLanguages: normalizedLegacyLanguages,
|
|
canonicalLegacyLanguages,
|
|
moduleIds,
|
|
};
|
|
}
|
|
|
|
function resolveInstallPlan(options = {}) {
|
|
const manifests = loadInstallManifests(options);
|
|
const profileId = options.profileId || null;
|
|
const explicitModuleIds = dedupeStrings(options.moduleIds);
|
|
const includedComponentIds = dedupeStrings(options.includeComponentIds);
|
|
const excludedComponentIds = dedupeStrings(options.excludeComponentIds);
|
|
const requestedModuleIds = [];
|
|
|
|
if (profileId) {
|
|
const profile = manifests.profiles[profileId];
|
|
if (!profile) {
|
|
throw new Error(`Unknown install profile: ${profileId}`);
|
|
}
|
|
requestedModuleIds.push(...profile.modules);
|
|
}
|
|
|
|
requestedModuleIds.push(...explicitModuleIds);
|
|
requestedModuleIds.push(...expandComponentIdsToModuleIds(includedComponentIds, manifests));
|
|
|
|
const excludedModuleIds = expandComponentIdsToModuleIds(excludedComponentIds, manifests);
|
|
const excludedModuleOwners = new Map();
|
|
for (const componentId of excludedComponentIds) {
|
|
const component = manifests.componentsById.get(componentId);
|
|
if (!component) {
|
|
throw new Error(`Unknown install component: ${componentId}`);
|
|
}
|
|
for (const moduleId of component.modules) {
|
|
const owners = excludedModuleOwners.get(moduleId) || [];
|
|
owners.push(componentId);
|
|
excludedModuleOwners.set(moduleId, owners);
|
|
}
|
|
}
|
|
|
|
const target = options.target || null;
|
|
if (target && !SUPPORTED_INSTALL_TARGETS.includes(target)) {
|
|
throw new Error(
|
|
`Unknown install target: ${target}. Expected one of ${SUPPORTED_INSTALL_TARGETS.join(', ')}`
|
|
);
|
|
}
|
|
|
|
const effectiveRequestedIds = dedupeStrings(
|
|
requestedModuleIds.filter(moduleId => !excludedModuleOwners.has(moduleId))
|
|
);
|
|
|
|
if (requestedModuleIds.length === 0) {
|
|
throw new Error('No install profile, module IDs, or included component IDs were provided');
|
|
}
|
|
|
|
if (effectiveRequestedIds.length === 0) {
|
|
throw new Error('Selection excludes every requested install module');
|
|
}
|
|
|
|
const selectedIds = new Set();
|
|
const skippedTargetIds = new Set();
|
|
const excludedIds = new Set(excludedModuleIds);
|
|
const visitingIds = new Set();
|
|
const resolvedIds = new Set();
|
|
|
|
function resolveModule(moduleId, dependencyOf, rootRequesterId) {
|
|
const module = manifests.modulesById.get(moduleId);
|
|
if (!module) {
|
|
throw new Error(`Unknown install module: ${moduleId}`);
|
|
}
|
|
|
|
if (excludedModuleOwners.has(moduleId)) {
|
|
if (dependencyOf) {
|
|
const owners = excludedModuleOwners.get(moduleId) || [];
|
|
throw new Error(
|
|
`Module ${dependencyOf} depends on excluded module ${moduleId}${owners.length > 0 ? ` (excluded by ${owners.join(', ')})` : ''}`
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (target && !module.targets.includes(target)) {
|
|
if (dependencyOf) {
|
|
skippedTargetIds.add(rootRequesterId || dependencyOf);
|
|
return false;
|
|
}
|
|
skippedTargetIds.add(moduleId);
|
|
return false;
|
|
}
|
|
|
|
if (resolvedIds.has(moduleId)) {
|
|
return true;
|
|
}
|
|
|
|
if (visitingIds.has(moduleId)) {
|
|
throw new Error(`Circular install dependency detected at ${moduleId}`);
|
|
}
|
|
|
|
visitingIds.add(moduleId);
|
|
for (const dependencyId of module.dependencies) {
|
|
const dependencyResolved = resolveModule(
|
|
dependencyId,
|
|
moduleId,
|
|
rootRequesterId || moduleId
|
|
);
|
|
if (!dependencyResolved) {
|
|
visitingIds.delete(moduleId);
|
|
if (!dependencyOf) {
|
|
skippedTargetIds.add(moduleId);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
visitingIds.delete(moduleId);
|
|
resolvedIds.add(moduleId);
|
|
selectedIds.add(moduleId);
|
|
return true;
|
|
}
|
|
|
|
for (const moduleId of effectiveRequestedIds) {
|
|
resolveModule(moduleId, null, moduleId);
|
|
}
|
|
|
|
const selectedModules = manifests.modules.filter(module => selectedIds.has(module.id));
|
|
const skippedModules = manifests.modules.filter(module => skippedTargetIds.has(module.id));
|
|
const excludedModules = manifests.modules.filter(module => excludedIds.has(module.id));
|
|
const scaffoldPlan = target
|
|
? planInstallTargetScaffold({
|
|
target,
|
|
repoRoot: manifests.repoRoot,
|
|
projectRoot: options.projectRoot || manifests.repoRoot,
|
|
homeDir: options.homeDir || os.homedir(),
|
|
modules: selectedModules,
|
|
})
|
|
: null;
|
|
|
|
return {
|
|
repoRoot: manifests.repoRoot,
|
|
profileId,
|
|
target,
|
|
requestedModuleIds: effectiveRequestedIds,
|
|
explicitModuleIds,
|
|
includedComponentIds,
|
|
excludedComponentIds,
|
|
selectedModuleIds: selectedModules.map(module => module.id),
|
|
skippedModuleIds: skippedModules.map(module => module.id),
|
|
excludedModuleIds: excludedModules.map(module => module.id),
|
|
selectedModules,
|
|
skippedModules,
|
|
excludedModules,
|
|
targetAdapterId: scaffoldPlan ? scaffoldPlan.adapter.id : null,
|
|
targetRoot: scaffoldPlan ? scaffoldPlan.targetRoot : null,
|
|
installStatePath: scaffoldPlan ? scaffoldPlan.installStatePath : null,
|
|
operations: scaffoldPlan ? scaffoldPlan.operations : [],
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_REPO_ROOT,
|
|
SUPPORTED_INSTALL_TARGETS,
|
|
getManifestPaths,
|
|
loadInstallManifests,
|
|
listInstallComponents,
|
|
listLegacyCompatibilityLanguages,
|
|
listInstallModules,
|
|
listInstallProfiles,
|
|
resolveInstallPlan,
|
|
resolveLegacyCompatibilitySelection,
|
|
validateInstallModuleIds,
|
|
};
|