feat: wire manifest resolution into install execution (#509)

This commit is contained in:
Affaan Mustafa
2026-03-15 21:47:22 -07:00
committed by GitHub
parent 8878c6d6b0
commit 1e0238de96
9 changed files with 417 additions and 68 deletions

View File

@@ -8,19 +8,16 @@
const { const {
SUPPORTED_INSTALL_TARGETS, SUPPORTED_INSTALL_TARGETS,
listAvailableLanguages, listLegacyCompatibilityLanguages,
} = require('./lib/install-executor'); } = require('./lib/install-manifests');
const { const {
LEGACY_INSTALL_TARGETS, LEGACY_INSTALL_TARGETS,
normalizeInstallRequest, normalizeInstallRequest,
parseInstallArgs, parseInstallArgs,
} = require('./lib/install/request'); } = require('./lib/install/request');
const { loadInstallConfig } = require('./lib/install/config');
const { applyInstallPlan } = require('./lib/install/apply');
const { createInstallPlanFromRequest } = require('./lib/install/runtime');
function showHelp(exitCode = 0) { function showHelp(exitCode = 0) {
const languages = listAvailableLanguages(); const languages = listLegacyCompatibilityLanguages();
console.log(` console.log(`
Usage: install.sh [--target <${LEGACY_INSTALL_TARGETS.join('|')}>] [--dry-run] [--json] <language> [<language> ...] Usage: install.sh [--target <${LEGACY_INSTALL_TARGETS.join('|')}>] [--dry-run] [--json] <language> [<language> ...]
@@ -61,6 +58,9 @@ function printHumanPlan(plan, dryRun) {
if (plan.mode === 'legacy') { if (plan.mode === 'legacy') {
console.log(`Languages: ${plan.languages.join(', ')}`); console.log(`Languages: ${plan.languages.join(', ')}`);
} else { } else {
if (plan.mode === 'legacy-compat') {
console.log(`Legacy languages: ${plan.legacyLanguages.join(', ')}`);
}
console.log(`Profile: ${plan.profileId || '(custom modules)'}`); console.log(`Profile: ${plan.profileId || '(custom modules)'}`);
console.log(`Included components: ${plan.includedComponentIds.join(', ') || '(none)'}`); console.log(`Included components: ${plan.includedComponentIds.join(', ') || '(none)'}`);
console.log(`Excluded components: ${plan.excludedComponentIds.join(', ') || '(none)'}`); console.log(`Excluded components: ${plan.excludedComponentIds.join(', ') || '(none)'}`);
@@ -100,6 +100,9 @@ function main() {
showHelp(0); showHelp(0);
} }
const { loadInstallConfig } = require('./lib/install/config');
const { applyInstallPlan } = require('./lib/install-executor');
const { createInstallPlanFromRequest } = require('./lib/install/runtime');
const config = options.configPath const config = options.configPath
? loadInstallConfig(options.configPath, { cwd: process.cwd() }) ? loadInstallConfig(options.configPath, { cwd: process.cwd() })
: null; : null;

View File

@@ -2,14 +2,14 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const { execFileSync } = require('child_process'); const { execFileSync } = require('child_process');
const { applyInstallPlan } = require('./install/apply');
const { LEGACY_INSTALL_TARGETS, parseInstallArgs } = require('./install/request'); const { LEGACY_INSTALL_TARGETS, parseInstallArgs } = require('./install/request');
const { const {
SUPPORTED_INSTALL_TARGETS, SUPPORTED_INSTALL_TARGETS,
listLegacyCompatibilityLanguages,
resolveLegacyCompatibilitySelection,
resolveInstallPlan, resolveInstallPlan,
} = require('./install-manifests'); } = require('./install-manifests');
const { getInstallTargetAdapter } = require('./install-targets/registry'); const { getInstallTargetAdapter } = require('./install-targets/registry');
const { createInstallState } = require('./install-state');
const LANGUAGE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/; const LANGUAGE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
const EXCLUDED_GENERATED_SOURCE_SUFFIXES = [ const EXCLUDED_GENERATED_SOURCE_SUFFIXES = [
@@ -68,8 +68,11 @@ function readDirectoryNames(dirPath) {
} }
function listAvailableLanguages(sourceRoot = getSourceRoot()) { function listAvailableLanguages(sourceRoot = getSourceRoot()) {
return readDirectoryNames(path.join(sourceRoot, 'rules')) return [...new Set([
.filter(name => name !== 'common'); ...listLegacyCompatibilityLanguages(),
...readDirectoryNames(path.join(sourceRoot, 'rules'))
.filter(name => name !== 'common'),
])].sort();
} }
function validateLegacyTarget(target) { function validateLegacyTarget(target) {
@@ -108,6 +111,16 @@ function isGeneratedRuntimeSourcePath(sourceRelativePath) {
return EXCLUDED_GENERATED_SOURCE_SUFFIXES.some(suffix => normalizedPath.endsWith(suffix)); return EXCLUDED_GENERATED_SOURCE_SUFFIXES.some(suffix => normalizedPath.endsWith(suffix));
} }
function createStatePreview(options) {
const { createInstallState } = require('./install-state');
return createInstallState(options);
}
function applyInstallPlan(plan) {
const { applyInstallPlan: applyPlan } = require('./install/apply');
return applyPlan(plan);
}
function buildCopyFileOperation({ moduleId, sourcePath, sourceRelativePath, destinationPath, strategy }) { function buildCopyFileOperation({ moduleId, sourcePath, sourceRelativePath, destinationPath, strategy }) {
return { return {
kind: 'copy-file', kind: 'copy-file',
@@ -449,7 +462,7 @@ function createLegacyInstallPlan(options = {}) {
manifestVersion: getManifestVersion(sourceRoot), manifestVersion: getManifestVersion(sourceRoot),
}; };
const statePreview = createInstallState({ const statePreview = createStatePreview({
adapter: plan.adapter, adapter: plan.adapter,
targetRoot: plan.targetRoot, targetRoot: plan.targetRoot,
installStatePath: plan.installStatePath, installStatePath: plan.installStatePath,
@@ -485,6 +498,38 @@ function createLegacyInstallPlan(options = {}) {
}; };
} }
function createLegacyCompatInstallPlan(options = {}) {
const sourceRoot = options.sourceRoot || getSourceRoot();
const projectRoot = options.projectRoot || process.cwd();
const target = options.target || 'claude';
validateLegacyTarget(target);
const selection = resolveLegacyCompatibilitySelection({
repoRoot: sourceRoot,
target,
legacyLanguages: options.legacyLanguages || [],
});
return createManifestInstallPlan({
sourceRoot,
projectRoot,
homeDir: options.homeDir,
target,
profileId: null,
moduleIds: selection.moduleIds,
includeComponentIds: [],
excludeComponentIds: [],
legacyLanguages: selection.legacyLanguages,
legacyMode: true,
requestProfileId: null,
requestModuleIds: [],
requestIncludeComponentIds: [],
requestExcludeComponentIds: [],
mode: 'legacy-compat',
});
}
function materializeScaffoldOperation(sourceRoot, operation) { function materializeScaffoldOperation(sourceRoot, operation) {
const sourcePath = path.join(sourceRoot, operation.sourceRelativePath); const sourcePath = path.join(sourceRoot, operation.sourceRelativePath);
if (!fs.existsSync(sourcePath)) { if (!fs.existsSync(sourcePath)) {
@@ -526,6 +571,21 @@ function createManifestInstallPlan(options = {}) {
const sourceRoot = options.sourceRoot || getSourceRoot(); const sourceRoot = options.sourceRoot || getSourceRoot();
const projectRoot = options.projectRoot || process.cwd(); const projectRoot = options.projectRoot || process.cwd();
const target = options.target || 'claude'; const target = options.target || 'claude';
const legacyLanguages = Array.isArray(options.legacyLanguages)
? [...options.legacyLanguages]
: [];
const requestProfileId = Object.hasOwn(options, 'requestProfileId')
? options.requestProfileId
: (options.profileId || null);
const requestModuleIds = Object.hasOwn(options, 'requestModuleIds')
? [...options.requestModuleIds]
: (Array.isArray(options.moduleIds) ? [...options.moduleIds] : []);
const requestIncludeComponentIds = Object.hasOwn(options, 'requestIncludeComponentIds')
? [...options.requestIncludeComponentIds]
: (Array.isArray(options.includeComponentIds) ? [...options.includeComponentIds] : []);
const requestExcludeComponentIds = Object.hasOwn(options, 'requestExcludeComponentIds')
? [...options.requestExcludeComponentIds]
: (Array.isArray(options.excludeComponentIds) ? [...options.excludeComponentIds] : []);
const plan = resolveInstallPlan({ const plan = resolveInstallPlan({
repoRoot: sourceRoot, repoRoot: sourceRoot,
projectRoot, projectRoot,
@@ -543,21 +603,17 @@ function createManifestInstallPlan(options = {}) {
repoCommit: getRepoCommit(sourceRoot), repoCommit: getRepoCommit(sourceRoot),
manifestVersion: getManifestVersion(sourceRoot), manifestVersion: getManifestVersion(sourceRoot),
}; };
const statePreview = createInstallState({ const statePreview = createStatePreview({
adapter, adapter,
targetRoot: plan.targetRoot, targetRoot: plan.targetRoot,
installStatePath: plan.installStatePath, installStatePath: plan.installStatePath,
request: { request: {
profile: plan.profileId, profile: requestProfileId,
modules: Array.isArray(options.moduleIds) ? [...options.moduleIds] : [], modules: requestModuleIds,
includeComponents: Array.isArray(options.includeComponentIds) includeComponents: requestIncludeComponentIds,
? [...options.includeComponentIds] excludeComponents: requestExcludeComponentIds,
: [], legacyLanguages,
excludeComponents: Array.isArray(options.excludeComponentIds) legacyMode: Boolean(options.legacyMode),
? [...options.excludeComponentIds]
: [],
legacyLanguages: [],
legacyMode: false,
}, },
resolution: { resolution: {
selectedModules: plan.selectedModuleIds, selectedModules: plan.selectedModuleIds,
@@ -568,7 +624,7 @@ function createManifestInstallPlan(options = {}) {
}); });
return { return {
mode: 'manifest', mode: options.mode || 'manifest',
target, target,
adapter: { adapter: {
id: adapter.id, id: adapter.id,
@@ -578,8 +634,9 @@ function createManifestInstallPlan(options = {}) {
targetRoot: plan.targetRoot, targetRoot: plan.targetRoot,
installRoot: plan.targetRoot, installRoot: plan.targetRoot,
installStatePath: plan.installStatePath, installStatePath: plan.installStatePath,
warnings: [], warnings: Array.isArray(options.warnings) ? [...options.warnings] : [],
languages: [], languages: legacyLanguages,
legacyLanguages,
profileId: plan.profileId, profileId: plan.profileId,
requestedModuleIds: plan.requestedModuleIds, requestedModuleIds: plan.requestedModuleIds,
explicitModuleIds: plan.explicitModuleIds, explicitModuleIds: plan.explicitModuleIds,
@@ -597,6 +654,7 @@ module.exports = {
SUPPORTED_INSTALL_TARGETS, SUPPORTED_INSTALL_TARGETS,
LEGACY_INSTALL_TARGETS, LEGACY_INSTALL_TARGETS,
applyInstallPlan, applyInstallPlan,
createLegacyCompatInstallPlan,
createManifestInstallPlan, createManifestInstallPlan,
createLegacyInstallPlan, createLegacyInstallPlan,
getSourceRoot, getSourceRoot,

View File

@@ -11,6 +11,50 @@ const COMPONENT_FAMILY_PREFIXES = {
framework: 'framework:', framework: 'framework:',
capability: 'capability:', capability: 'capability:',
}; };
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) { function readJson(filePath, label) {
try { try {
@@ -24,6 +68,19 @@ function dedupeStrings(values) {
return [...new Set((Array.isArray(values) ? values : []).map(value => String(value).trim()).filter(Boolean))]; 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) { function intersectTargets(modules) {
if (!Array.isArray(modules) || modules.length === 0) { if (!Array.isArray(modules) || modules.length === 0) {
return []; return [];
@@ -102,6 +159,17 @@ function listInstallModules(options = {}) {
})); }));
} }
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 = {}) { function listInstallComponents(options = {}) {
const manifests = loadInstallManifests(options); const manifests = loadInstallManifests(options);
const family = options.family || null; const family = options.family || null;
@@ -154,6 +222,59 @@ function expandComponentIdsToModuleIds(componentIds, manifests) {
return dedupeStrings(expandedModuleIds); 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 = {}) { function resolveInstallPlan(options = {}) {
const manifests = loadInstallManifests(options); const manifests = loadInstallManifests(options);
const profileId = options.profileId || null; const profileId = options.profileId || null;
@@ -212,7 +333,7 @@ function resolveInstallPlan(options = {}) {
const visitingIds = new Set(); const visitingIds = new Set();
const resolvedIds = new Set(); const resolvedIds = new Set();
function resolveModule(moduleId, dependencyOf) { function resolveModule(moduleId, dependencyOf, rootRequesterId) {
const module = manifests.modulesById.get(moduleId); const module = manifests.modulesById.get(moduleId);
if (!module) { if (!module) {
throw new Error(`Unknown install module: ${moduleId}`); throw new Error(`Unknown install module: ${moduleId}`);
@@ -230,16 +351,15 @@ function resolveInstallPlan(options = {}) {
if (target && !module.targets.includes(target)) { if (target && !module.targets.includes(target)) {
if (dependencyOf) { if (dependencyOf) {
throw new Error( skippedTargetIds.add(rootRequesterId || dependencyOf);
`Module ${dependencyOf} depends on ${moduleId}, which does not support target ${target}` return false;
);
} }
skippedTargetIds.add(moduleId); skippedTargetIds.add(moduleId);
return; return false;
} }
if (resolvedIds.has(moduleId)) { if (resolvedIds.has(moduleId)) {
return; return true;
} }
if (visitingIds.has(moduleId)) { if (visitingIds.has(moduleId)) {
@@ -248,15 +368,27 @@ function resolveInstallPlan(options = {}) {
visitingIds.add(moduleId); visitingIds.add(moduleId);
for (const dependencyId of module.dependencies) { for (const dependencyId of module.dependencies) {
resolveModule(dependencyId, moduleId); const dependencyResolved = resolveModule(
dependencyId,
moduleId,
rootRequesterId || moduleId
);
if (!dependencyResolved) {
visitingIds.delete(moduleId);
if (!dependencyOf) {
skippedTargetIds.add(moduleId);
}
return false;
}
} }
visitingIds.delete(moduleId); visitingIds.delete(moduleId);
resolvedIds.add(moduleId); resolvedIds.add(moduleId);
selectedIds.add(moduleId); selectedIds.add(moduleId);
return true;
} }
for (const moduleId of effectiveRequestedIds) { for (const moduleId of effectiveRequestedIds) {
resolveModule(moduleId, null); resolveModule(moduleId, null, moduleId);
} }
const selectedModules = manifests.modules.filter(module => selectedIds.has(module.id)); const selectedModules = manifests.modules.filter(module => selectedIds.has(module.id));
@@ -299,7 +431,10 @@ module.exports = {
getManifestPaths, getManifestPaths,
loadInstallManifests, loadInstallManifests,
listInstallComponents, listInstallComponents,
listLegacyCompatibilityLanguages,
listInstallModules, listInstallModules,
listInstallProfiles, listInstallProfiles,
resolveInstallPlan, resolveInstallPlan,
resolveLegacyCompatibilitySelection,
validateInstallModuleIds,
}; };

View File

@@ -1,5 +1,7 @@
'use strict'; 'use strict';
const { validateInstallModuleIds } = require('../install-manifests');
const LEGACY_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity']; const LEGACY_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity'];
function dedupeStrings(values) { function dedupeStrings(values) {
@@ -35,7 +37,7 @@ function parseInstallArgs(argv) {
index += 1; index += 1;
} else if (arg === '--modules') { } else if (arg === '--modules') {
const raw = args[index + 1] || ''; const raw = args[index + 1] || '';
parsed.moduleIds = raw.split(',').map(value => value.trim()).filter(Boolean); parsed.moduleIds = dedupeStrings(raw.split(','));
index += 1; index += 1;
} else if (arg === '--with') { } else if (arg === '--with') {
const componentId = args[index + 1] || ''; const componentId = args[index + 1] || '';
@@ -70,7 +72,9 @@ function normalizeInstallRequest(options = {}) {
? options.config ? options.config
: null; : null;
const profileId = options.profileId || config?.profileId || null; const profileId = options.profileId || config?.profileId || null;
const moduleIds = dedupeStrings([...(config?.moduleIds || []), ...(options.moduleIds || [])]); const moduleIds = validateInstallModuleIds(
dedupeStrings([...(config?.moduleIds || []), ...(options.moduleIds || [])])
);
const includeComponentIds = dedupeStrings([ const includeComponentIds = dedupeStrings([
...(config?.includeComponentIds || []), ...(config?.includeComponentIds || []),
...(options.includeComponentIds || []), ...(options.includeComponentIds || []),
@@ -79,29 +83,32 @@ function normalizeInstallRequest(options = {}) {
...(config?.excludeComponentIds || []), ...(config?.excludeComponentIds || []),
...(options.excludeComponentIds || []), ...(options.excludeComponentIds || []),
]); ]);
const languages = Array.isArray(options.languages) ? [...options.languages] : []; const legacyLanguages = dedupeStrings(dedupeStrings([
...(Array.isArray(options.legacyLanguages) ? options.legacyLanguages : []),
...(Array.isArray(options.languages) ? options.languages : []),
]).map(language => language.toLowerCase()));
const target = options.target || config?.target || 'claude'; const target = options.target || config?.target || 'claude';
const hasManifestBaseSelection = Boolean(profileId) || moduleIds.length > 0 || includeComponentIds.length > 0; const hasManifestBaseSelection = Boolean(profileId) || moduleIds.length > 0 || includeComponentIds.length > 0;
const usingManifestMode = hasManifestBaseSelection || excludeComponentIds.length > 0; const usingManifestMode = hasManifestBaseSelection || excludeComponentIds.length > 0;
if (usingManifestMode && languages.length > 0) { if (usingManifestMode && legacyLanguages.length > 0) {
throw new Error( throw new Error(
'Legacy language arguments cannot be combined with --profile, --modules, --with, --without, or manifest config selections' 'Legacy language arguments cannot be combined with --profile, --modules, --with, --without, or manifest config selections'
); );
} }
if (!options.help && !hasManifestBaseSelection && languages.length === 0) { if (!options.help && !hasManifestBaseSelection && legacyLanguages.length === 0) {
throw new Error('No install profile, module IDs, included components, or legacy languages were provided'); throw new Error('No install profile, module IDs, included components, or legacy languages were provided');
} }
return { return {
mode: usingManifestMode ? 'manifest' : 'legacy', mode: usingManifestMode ? 'manifest' : 'legacy-compat',
target, target,
profileId, profileId,
moduleIds, moduleIds,
includeComponentIds, includeComponentIds,
excludeComponentIds, excludeComponentIds,
languages, legacyLanguages,
configPath: config?.path || options.configPath || null, configPath: config?.path || options.configPath || null,
}; };
} }

View File

@@ -1,6 +1,7 @@
'use strict'; 'use strict';
const { const {
createLegacyCompatInstallPlan,
createLegacyInstallPlan, createLegacyInstallPlan,
createManifestInstallPlan, createManifestInstallPlan,
} = require('../install-executor'); } = require('../install-executor');
@@ -23,6 +24,17 @@ function createInstallPlanFromRequest(request, options = {}) {
}); });
} }
if (request.mode === 'legacy-compat') {
return createLegacyCompatInstallPlan({
target: request.target,
legacyLanguages: request.legacyLanguages,
projectRoot: options.projectRoot,
homeDir: options.homeDir,
claudeRulesDir: options.claudeRulesDir,
sourceRoot: options.sourceRoot,
});
}
if (request.mode === 'legacy') { if (request.mode === 'legacy') {
return createLegacyInstallPlan({ return createLegacyInstallPlan({
target: request.target, target: request.target,

View File

@@ -10,9 +10,12 @@ const path = require('path');
const { const {
loadInstallManifests, loadInstallManifests,
listInstallComponents, listInstallComponents,
listLegacyCompatibilityLanguages,
listInstallModules, listInstallModules,
listInstallProfiles, listInstallProfiles,
resolveInstallPlan, resolveInstallPlan,
resolveLegacyCompatibilitySelection,
validateInstallModuleIds,
} = require('../../scripts/lib/install-manifests'); } = require('../../scripts/lib/install-manifests');
function test(name, fn) { function test(name, fn) {
@@ -75,6 +78,15 @@ function runTests() {
'Should include capability:security'); 'Should include capability:security');
})) passed++; else failed++; })) passed++; else failed++;
if (test('lists supported legacy compatibility languages', () => {
const languages = listLegacyCompatibilityLanguages();
assert.ok(languages.includes('typescript'));
assert.ok(languages.includes('python'));
assert.ok(languages.includes('go'));
assert.ok(languages.includes('golang'));
assert.ok(languages.includes('kotlin'));
})) passed++; else failed++;
if (test('resolves a real project profile with target-specific skips', () => { if (test('resolves a real project profile with target-specific skips', () => {
const projectRoot = '/workspace/app'; const projectRoot = '/workspace/app';
const plan = resolveInstallPlan({ profileId: 'developer', target: 'cursor', projectRoot }); const plan = resolveInstallPlan({ profileId: 'developer', target: 'cursor', projectRoot });
@@ -97,6 +109,18 @@ function runTests() {
); );
})) passed++; else failed++; })) passed++; else failed++;
if (test('resolves antigravity profiles by skipping incompatible dependency trees', () => {
const projectRoot = '/workspace/app';
const plan = resolveInstallPlan({ profileId: 'core', target: 'antigravity', projectRoot });
assert.deepStrictEqual(plan.selectedModuleIds, ['rules-core', 'agents-core', 'commands-core']);
assert.ok(plan.skippedModuleIds.includes('hooks-runtime'));
assert.ok(plan.skippedModuleIds.includes('platform-configs'));
assert.ok(plan.skippedModuleIds.includes('workflow-quality'));
assert.strictEqual(plan.targetAdapterId, 'antigravity-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.agent'));
})) passed++; else failed++;
if (test('resolves explicit modules with dependency expansion', () => { if (test('resolves explicit modules with dependency expansion', () => {
const plan = resolveInstallPlan({ moduleIds: ['security'] }); const plan = resolveInstallPlan({ moduleIds: ['security'] });
assert.ok(plan.selectedModuleIds.includes('security'), 'Should include requested module'); assert.ok(plan.selectedModuleIds.includes('security'), 'Should include requested module');
@@ -106,6 +130,50 @@ function runTests() {
'Should include nested dependency'); 'Should include nested dependency');
})) passed++; else failed++; })) passed++; else failed++;
if (test('validates explicit module IDs against the real manifest catalog', () => {
const moduleIds = validateInstallModuleIds(['security', 'security', 'platform-configs']);
assert.deepStrictEqual(moduleIds, ['security', 'platform-configs']);
assert.throws(
() => validateInstallModuleIds(['ghost-module']),
/Unknown install module: ghost-module/
);
})) passed++; else failed++;
if (test('resolves legacy compatibility selections into manifest module IDs', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'cursor',
legacyLanguages: ['typescript', 'go', 'golang'],
});
assert.deepStrictEqual(selection.legacyLanguages, ['typescript', 'go', 'golang']);
assert.ok(selection.moduleIds.includes('rules-core'));
assert.ok(selection.moduleIds.includes('agents-core'));
assert.ok(selection.moduleIds.includes('commands-core'));
assert.ok(selection.moduleIds.includes('hooks-runtime'));
assert.ok(selection.moduleIds.includes('platform-configs'));
assert.ok(selection.moduleIds.includes('workflow-quality'));
assert.ok(selection.moduleIds.includes('framework-language'));
})) passed++; else failed++;
if (test('keeps antigravity legacy compatibility selections target-safe', () => {
const selection = resolveLegacyCompatibilitySelection({
target: 'antigravity',
legacyLanguages: ['typescript'],
});
assert.deepStrictEqual(selection.moduleIds, ['rules-core', 'agents-core', 'commands-core']);
})) passed++; else failed++;
if (test('rejects unknown legacy compatibility languages', () => {
assert.throws(
() => resolveLegacyCompatibilitySelection({
target: 'cursor',
legacyLanguages: ['brainfuck'],
}),
/Unknown legacy language: brainfuck/
);
})) passed++; else failed++;
if (test('resolves included and excluded user-facing components', () => { if (test('resolves included and excluded user-facing components', () => {
const plan = resolveInstallPlan({ const plan = resolveInstallPlan({
profileId: 'core', profileId: 'core',
@@ -146,7 +214,7 @@ function runTests() {
); );
})) passed++; else failed++; })) passed++; else failed++;
if (test('throws when a dependency does not support the requested target', () => { if (test('skips a requested module when its dependency chain does not support the target', () => {
const repoRoot = createTestRepo(); const repoRoot = createTestRepo();
writeJson(path.join(repoRoot, 'manifests', 'install-modules.json'), { writeJson(path.join(repoRoot, 'manifests', 'install-modules.json'), {
version: 1, version: 1,
@@ -182,10 +250,9 @@ function runTests() {
} }
}); });
assert.throws( const plan = resolveInstallPlan({ repoRoot, profileId: 'core', target: 'claude' });
() => resolveInstallPlan({ repoRoot, profileId: 'core', target: 'claude' }), assert.deepStrictEqual(plan.selectedModuleIds, []);
/does not support target claude/ assert.deepStrictEqual(plan.skippedModuleIds, ['parent']);
);
cleanupTestRepo(repoRoot); cleanupTestRepo(repoRoot);
})) passed++; else failed++; })) passed++; else failed++;

View File

@@ -33,6 +33,7 @@ function runTests() {
'scripts/install-apply.js', 'scripts/install-apply.js',
'--target', 'cursor', '--target', 'cursor',
'--profile', 'developer', '--profile', 'developer',
'--modules', 'platform-configs, workflow-quality ,platform-configs',
'--with', 'lang:typescript', '--with', 'lang:typescript',
'--without', 'capability:media', '--without', 'capability:media',
'--config', 'ecc-install.json', '--config', 'ecc-install.json',
@@ -43,6 +44,7 @@ function runTests() {
assert.strictEqual(parsed.target, 'cursor'); assert.strictEqual(parsed.target, 'cursor');
assert.strictEqual(parsed.profileId, 'developer'); assert.strictEqual(parsed.profileId, 'developer');
assert.strictEqual(parsed.configPath, 'ecc-install.json'); assert.strictEqual(parsed.configPath, 'ecc-install.json');
assert.deepStrictEqual(parsed.moduleIds, ['platform-configs', 'workflow-quality']);
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript']); assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:media']); assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:media']);
assert.strictEqual(parsed.dryRun, true); assert.strictEqual(parsed.dryRun, true);
@@ -58,9 +60,9 @@ function runTests() {
languages: ['typescript', 'python'] languages: ['typescript', 'python']
}); });
assert.strictEqual(request.mode, 'legacy'); assert.strictEqual(request.mode, 'legacy-compat');
assert.strictEqual(request.target, 'claude'); assert.strictEqual(request.target, 'claude');
assert.deepStrictEqual(request.languages, ['typescript', 'python']); assert.deepStrictEqual(request.legacyLanguages, ['typescript', 'python']);
assert.deepStrictEqual(request.moduleIds, []); assert.deepStrictEqual(request.moduleIds, []);
assert.strictEqual(request.profileId, null); assert.strictEqual(request.profileId, null);
})) passed++; else failed++; })) passed++; else failed++;
@@ -80,7 +82,7 @@ function runTests() {
assert.strictEqual(request.profileId, 'developer'); assert.strictEqual(request.profileId, 'developer');
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript']); assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(request.excludeComponentIds, ['capability:media']); assert.deepStrictEqual(request.excludeComponentIds, ['capability:media']);
assert.deepStrictEqual(request.languages, []); assert.deepStrictEqual(request.legacyLanguages, []);
})) passed++; else failed++; })) passed++; else failed++;
if (test('merges config-backed component selections with CLI overrides', () => { if (test('merges config-backed component selections with CLI overrides', () => {
@@ -111,6 +113,20 @@ function runTests() {
assert.strictEqual(request.configPath, '/workspace/app/ecc-install.json'); assert.strictEqual(request.configPath, '/workspace/app/ecc-install.json');
})) passed++; else failed++; })) passed++; else failed++;
if (test('validates explicit module IDs against the manifest catalog', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'cursor',
profileId: null,
moduleIds: ['ghost-module'],
includeComponentIds: [],
excludeComponentIds: [],
languages: [],
}),
/Unknown install module: ghost-module/
);
})) passed++; else failed++;
if (test('rejects mixing legacy languages with manifest flags', () => { if (test('rejects mixing legacy languages with manifest flags', () => {
assert.throws( assert.throws(
() => normalizeInstallRequest({ () => normalizeInstallRequest({

View File

@@ -60,16 +60,18 @@ function main() {
assert.strictEqual(result.status, 0, result.stderr); assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout); const payload = parseJson(result.stdout);
assert.strictEqual(payload.dryRun, true); assert.strictEqual(payload.dryRun, true);
assert.strictEqual(payload.plan.mode, 'legacy'); assert.strictEqual(payload.plan.mode, 'legacy-compat');
assert.deepStrictEqual(payload.plan.languages, ['typescript']); assert.deepStrictEqual(payload.plan.legacyLanguages, ['typescript']);
assert.ok(payload.plan.selectedModuleIds.includes('framework-language'));
}], }],
['routes implicit top-level args to install', () => { ['routes implicit top-level args to install', () => {
const result = runCli(['--dry-run', '--json', 'typescript']); const result = runCli(['--dry-run', '--json', 'typescript']);
assert.strictEqual(result.status, 0, result.stderr); assert.strictEqual(result.status, 0, result.stderr);
const payload = parseJson(result.stdout); const payload = parseJson(result.stdout);
assert.strictEqual(payload.dryRun, true); assert.strictEqual(payload.dryRun, true);
assert.strictEqual(payload.plan.mode, 'legacy'); assert.strictEqual(payload.plan.mode, 'legacy-compat');
assert.deepStrictEqual(payload.plan.languages, ['typescript']); assert.deepStrictEqual(payload.plan.legacyLanguages, ['typescript']);
assert.ok(payload.plan.selectedModuleIds.includes('framework-language'));
}], }],
['delegates plan command', () => { ['delegates plan command', () => {
const result = runCli(['plan', '--list-profiles', '--json']); const result = runCli(['plan', '--list-profiles', '--json']);

View File

@@ -89,18 +89,26 @@ function runTests() {
const result = run(['typescript'], { cwd: projectDir, homeDir }); const result = run(['typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr); assert.strictEqual(result.code, 0, result.stderr);
const rulesDir = path.join(homeDir, '.claude', 'rules'); const claudeRoot = path.join(homeDir, '.claude');
assert.ok(fs.existsSync(path.join(rulesDir, 'common', 'coding-style.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(rulesDir, 'typescript', 'testing.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'typescript', 'testing.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'hooks', 'session-end.js')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'plugin.json')));
const statePath = path.join(homeDir, '.claude', 'ecc', 'install-state.json'); const statePath = path.join(homeDir, '.claude', 'ecc', 'install-state.json');
const state = readJson(statePath); const state = readJson(statePath);
assert.strictEqual(state.target.id, 'claude-home'); assert.strictEqual(state.target.id, 'claude-home');
assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']); assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']);
assert.strictEqual(state.request.legacyMode, true); assert.strictEqual(state.request.legacyMode, true);
assert.deepStrictEqual(state.request.modules, []);
assert.ok(state.resolution.selectedModules.includes('rules-core'));
assert.ok(state.resolution.selectedModules.includes('framework-language'));
assert.ok( assert.ok(
state.operations.some(operation => ( state.operations.some(operation => (
operation.destinationPath === path.join(rulesDir, 'common', 'coding-style.md') operation.destinationPath === path.join(claudeRoot, 'rules', 'common', 'coding-style.md')
)), )),
'Should record common rule file operation' 'Should record common rule file operation'
); );
@@ -118,22 +126,28 @@ function runTests() {
const result = run(['--target', 'cursor', 'typescript'], { cwd: projectDir, homeDir }); const result = run(['--target', 'cursor', 'typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr); assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-coding-style.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'typescript-testing.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'typescript', 'testing.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks', 'session-start.js'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks', 'session-start.js')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'article-writing', 'SKILL.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'skills', 'coding-standards', 'SKILL.md')));
const statePath = path.join(projectDir, '.cursor', 'ecc-install-state.json'); const statePath = path.join(projectDir, '.cursor', 'ecc-install-state.json');
const state = readJson(statePath); const state = readJson(statePath);
const normalizedProjectDir = fs.realpathSync(projectDir); const normalizedProjectDir = fs.realpathSync(projectDir);
assert.strictEqual(state.target.id, 'cursor-project'); assert.strictEqual(state.target.id, 'cursor-project');
assert.strictEqual(state.target.root, path.join(normalizedProjectDir, '.cursor')); assert.strictEqual(state.target.root, path.join(normalizedProjectDir, '.cursor'));
assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']);
assert.strictEqual(state.request.legacyMode, true);
assert.ok(state.resolution.selectedModules.includes('framework-language'));
assert.ok( assert.ok(
state.operations.some(operation => ( state.operations.some(operation => (
operation.destinationPath === path.join(normalizedProjectDir, '.cursor', 'hooks', 'session-start.js') operation.destinationPath === path.join(normalizedProjectDir, '.cursor', 'commands', 'plan.md')
)), )),
'Should record hook file copy operation' 'Should record manifest command file copy operation'
); );
} finally { } finally {
cleanup(homeDir); cleanup(homeDir);
@@ -149,20 +163,22 @@ function runTests() {
const result = run(['--target', 'antigravity', 'typescript'], { cwd: projectDir, homeDir }); const result = run(['--target', 'antigravity', 'typescript'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr); assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common-coding-style.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'typescript-testing.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'typescript', 'testing.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'workflows', 'code-review.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'article-writing', 'SKILL.md')));
const statePath = path.join(projectDir, '.agent', 'ecc-install-state.json'); const statePath = path.join(projectDir, '.agent', 'ecc-install-state.json');
const state = readJson(statePath); const state = readJson(statePath);
assert.strictEqual(state.target.id, 'antigravity-project'); assert.strictEqual(state.target.id, 'antigravity-project');
assert.deepStrictEqual(state.request.legacyLanguages, ['typescript']);
assert.strictEqual(state.request.legacyMode, true);
assert.deepStrictEqual(state.resolution.selectedModules, ['rules-core', 'agents-core', 'commands-core']);
assert.ok( assert.ok(
state.operations.some(operation => ( state.operations.some(operation => (
operation.destinationPath.endsWith(path.join('.agent', 'workflows', 'code-review.md')) operation.destinationPath.endsWith(path.join('.agent', 'commands', 'plan.md'))
)), )),
'Should record workflow file copy operation' 'Should record manifest command file copy operation'
); );
} finally { } finally {
cleanup(homeDir); cleanup(homeDir);
@@ -181,6 +197,8 @@ function runTests() {
}); });
assert.strictEqual(result.code, 0, result.stderr); assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('Dry-run install plan')); assert.ok(result.stdout.includes('Dry-run install plan'));
assert.ok(result.stdout.includes('Mode: legacy-compat'));
assert.ok(result.stdout.includes('Legacy languages: typescript'));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json'))); assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json')));
assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'ecc-install-state.json'))); assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'ecc-install-state.json')));
} finally { } finally {
@@ -240,6 +258,31 @@ function runTests() {
} }
})) passed++; else failed++; })) passed++; else failed++;
if (test('installs antigravity manifest profiles while skipping incompatible modules', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const result = run(['--target', 'antigravity', '--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'commands', 'plan.md')));
assert.ok(!fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md')));
const state = readJson(path.join(projectDir, '.agent', 'ecc-install-state.json'));
assert.strictEqual(state.request.profile, 'core');
assert.strictEqual(state.request.legacyMode, false);
assert.deepStrictEqual(state.resolution.selectedModules, ['rules-core', 'agents-core', 'commands-core']);
assert.ok(state.resolution.skippedModules.includes('workflow-quality'));
assert.ok(state.resolution.skippedModules.includes('platform-configs'));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs explicit modules for cursor using manifest operations', () => { if (test('installs explicit modules for cursor using manifest operations', () => {
const homeDir = createTempDir('install-apply-home-'); const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-'); const projectDir = createTempDir('install-apply-project-');
@@ -270,6 +313,12 @@ function runTests() {
} }
})) passed++; else failed++; })) passed++; else failed++;
if (test('rejects unknown explicit manifest modules before resolution', () => {
const result = run(['--modules', 'ghost-module']);
assert.strictEqual(result.code, 1);
assert.ok(result.stderr.includes('Unknown install module: ghost-module'));
})) passed++; else failed++;
if (test('installs from ecc-install.json and persists component selections', () => { if (test('installs from ecc-install.json and persists component selections', () => {
const homeDir = createTempDir('install-apply-home-'); const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-'); const projectDir = createTempDir('install-apply-project-');