feat: implement --with/--without selective install flags (#679)

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
This commit is contained in:
Affaan Mustafa
2026-03-20 00:43:32 -07:00
committed by GitHub
parent 29277ac273
commit 07f6156d8a
4 changed files with 876 additions and 2 deletions

View File

@@ -250,6 +250,158 @@
"modules": [ "modules": [
"document-processing" "document-processing"
] ]
},
{
"id": "agent:architect",
"family": "agent",
"description": "System design and architecture agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:code-reviewer",
"family": "agent",
"description": "Code review agent for quality and security checks.",
"modules": [
"agents-core"
]
},
{
"id": "agent:security-reviewer",
"family": "agent",
"description": "Security vulnerability analysis agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:tdd-guide",
"family": "agent",
"description": "Test-driven development guidance agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:planner",
"family": "agent",
"description": "Feature implementation planning agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:build-error-resolver",
"family": "agent",
"description": "Build error resolution agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:e2e-runner",
"family": "agent",
"description": "Playwright E2E testing agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:refactor-cleaner",
"family": "agent",
"description": "Dead code cleanup and refactoring agent.",
"modules": [
"agents-core"
]
},
{
"id": "agent:doc-updater",
"family": "agent",
"description": "Documentation update agent.",
"modules": [
"agents-core"
]
},
{
"id": "skill:tdd-workflow",
"family": "skill",
"description": "Test-driven development workflow skill.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:continuous-learning",
"family": "skill",
"description": "Session pattern extraction and continuous learning skill.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:eval-harness",
"family": "skill",
"description": "Evaluation harness for AI regression testing.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:verification-loop",
"family": "skill",
"description": "Verification loop for code quality assurance.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:strategic-compact",
"family": "skill",
"description": "Strategic context compaction for long sessions.",
"modules": [
"workflow-quality"
]
},
{
"id": "skill:coding-standards",
"family": "skill",
"description": "Language-agnostic coding standards and best practices.",
"modules": [
"framework-language"
]
},
{
"id": "skill:frontend-patterns",
"family": "skill",
"description": "React and frontend engineering patterns.",
"modules": [
"framework-language"
]
},
{
"id": "skill:backend-patterns",
"family": "skill",
"description": "API design, database, and backend engineering patterns.",
"modules": [
"framework-language"
]
},
{
"id": "skill:security-review",
"family": "skill",
"description": "Security review checklist and vulnerability analysis.",
"modules": [
"security"
]
},
{
"id": "skill:deep-research",
"family": "skill",
"description": "Deep research and investigation workflows.",
"modules": [
"research-apis"
]
} }
] ]
} }

View File

@@ -26,7 +26,7 @@
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
"pattern": "^(baseline|lang|framework|capability):[a-z0-9-]+$" "pattern": "^(baseline|lang|framework|capability|agent|skill):[a-z0-9-]+$"
}, },
"family": { "family": {
"type": "string", "type": "string",
@@ -34,7 +34,9 @@
"baseline", "baseline",
"language", "language",
"framework", "framework",
"capability" "capability",
"agent",
"skill"
] ]
}, },
"description": { "description": {

View File

@@ -10,6 +10,8 @@ const COMPONENT_FAMILY_PREFIXES = {
language: 'lang:', language: 'lang:',
framework: 'framework:', framework: 'framework:',
capability: 'capability:', capability: 'capability:',
agent: 'agent:',
skill: 'skill:',
}; };
const LEGACY_COMPAT_BASE_MODULE_IDS_BY_TARGET = Object.freeze({ const LEGACY_COMPAT_BASE_MODULE_IDS_BY_TARGET = Object.freeze({
claude: [ claude: [

View File

@@ -0,0 +1,718 @@
/**
* Tests for --with / --without selective install flags (issue #470)
*
* Covers:
* - CLI argument parsing for --with and --without
* - Request normalization with include/exclude component IDs
* - Component-to-module expansion via the manifest catalog
* - End-to-end install plans with --with and --without
* - Validation and error handling for unknown component IDs
* - Combined --profile + --with + --without flows
* - Standalone --with without a profile
* - agent: and skill: component families
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
parseInstallArgs,
normalizeInstallRequest,
} = require('../../scripts/lib/install/request');
const {
loadInstallManifests,
listInstallComponents,
resolveInstallPlan,
} = require('../../scripts/lib/install-manifests');
function test(name, fn) {
try {
fn();
console.log(` \u2713 ${name}`);
return true;
} catch (error) {
console.log(` \u2717 ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing --with / --without selective install flags ===\n');
let passed = 0;
let failed = 0;
// ─── CLI Argument Parsing ───
if (test('parses single --with flag', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'core',
'--with', 'lang:typescript',
]);
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(parsed.excludeComponentIds, []);
})) passed++; else failed++;
if (test('parses single --without flag', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'developer',
'--without', 'capability:orchestration',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:orchestration']);
assert.deepStrictEqual(parsed.includeComponentIds, []);
})) passed++; else failed++;
if (test('parses multiple --with flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--with', 'lang:typescript',
'--with', 'framework:nextjs',
'--with', 'capability:database',
]);
assert.deepStrictEqual(parsed.includeComponentIds, [
'lang:typescript',
'framework:nextjs',
'capability:database',
]);
})) passed++; else failed++;
if (test('parses multiple --without flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'full',
'--without', 'capability:media',
'--without', 'capability:social',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, [
'capability:media',
'capability:social',
]);
})) passed++; else failed++;
if (test('parses combined --with and --without flags', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'developer',
'--with', 'lang:typescript',
'--with', 'framework:nextjs',
'--without', 'capability:orchestration',
]);
assert.strictEqual(parsed.profileId, 'developer');
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:typescript', 'framework:nextjs']);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:orchestration']);
})) passed++; else failed++;
if (test('ignores empty --with values', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--with', '',
'--with', 'lang:python',
]);
assert.deepStrictEqual(parsed.includeComponentIds, ['lang:python']);
})) passed++; else failed++;
if (test('ignores empty --without values', () => {
const parsed = parseInstallArgs([
'node', 'install-apply.js',
'--profile', 'core',
'--without', '',
'--without', 'capability:media',
]);
assert.deepStrictEqual(parsed.excludeComponentIds, ['capability:media']);
})) passed++; else failed++;
// ─── Request Normalization ───
if (test('normalizes --with-only request as manifest mode', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript'],
excludeComponentIds: [],
languages: [],
});
assert.strictEqual(request.mode, 'manifest');
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript']);
assert.deepStrictEqual(request.excludeComponentIds, []);
})) passed++; else failed++;
if (test('normalizes --profile + --with + --without as manifest mode', () => {
const request = normalizeInstallRequest({
target: 'cursor',
profileId: 'developer',
moduleIds: [],
includeComponentIds: ['lang:typescript', 'framework:nextjs'],
excludeComponentIds: ['capability:orchestration'],
languages: [],
});
assert.strictEqual(request.mode, 'manifest');
assert.strictEqual(request.profileId, 'developer');
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'framework:nextjs']);
assert.deepStrictEqual(request.excludeComponentIds, ['capability:orchestration']);
})) passed++; else failed++;
if (test('rejects --with combined with legacy language arguments', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript'],
excludeComponentIds: [],
languages: ['python'],
}),
/cannot be combined/
);
})) passed++; else failed++;
if (test('rejects --without combined with legacy language arguments', () => {
assert.throws(
() => normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: [],
excludeComponentIds: ['capability:media'],
languages: ['typescript'],
}),
/cannot be combined/
);
})) passed++; else failed++;
if (test('deduplicates repeated --with component IDs', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: null,
moduleIds: [],
includeComponentIds: ['lang:typescript', 'lang:typescript', 'lang:python'],
excludeComponentIds: [],
languages: [],
});
assert.deepStrictEqual(request.includeComponentIds, ['lang:typescript', 'lang:python']);
})) passed++; else failed++;
if (test('deduplicates repeated --without component IDs', () => {
const request = normalizeInstallRequest({
target: 'claude',
profileId: 'full',
moduleIds: [],
includeComponentIds: [],
excludeComponentIds: ['capability:media', 'capability:media', 'capability:social'],
languages: [],
});
assert.deepStrictEqual(request.excludeComponentIds, ['capability:media', 'capability:social']);
})) passed++; else failed++;
// ─── Component Catalog Validation ───
if (test('component catalog includes lang: family entries', () => {
const components = listInstallComponents({ family: 'language' });
assert.ok(components.some(c => c.id === 'lang:typescript'), 'Should have lang:typescript');
assert.ok(components.some(c => c.id === 'lang:python'), 'Should have lang:python');
assert.ok(components.some(c => c.id === 'lang:go'), 'Should have lang:go');
assert.ok(components.some(c => c.id === 'lang:java'), 'Should have lang:java');
})) passed++; else failed++;
if (test('component catalog includes framework: family entries', () => {
const components = listInstallComponents({ family: 'framework' });
assert.ok(components.some(c => c.id === 'framework:react'), 'Should have framework:react');
assert.ok(components.some(c => c.id === 'framework:nextjs'), 'Should have framework:nextjs');
assert.ok(components.some(c => c.id === 'framework:django'), 'Should have framework:django');
assert.ok(components.some(c => c.id === 'framework:springboot'), 'Should have framework:springboot');
})) passed++; else failed++;
if (test('component catalog includes capability: family entries', () => {
const components = listInstallComponents({ family: 'capability' });
assert.ok(components.some(c => c.id === 'capability:database'), 'Should have capability:database');
assert.ok(components.some(c => c.id === 'capability:security'), 'Should have capability:security');
assert.ok(components.some(c => c.id === 'capability:orchestration'), 'Should have capability:orchestration');
})) passed++; else failed++;
if (test('component catalog includes agent: family entries', () => {
const components = listInstallComponents({ family: 'agent' });
assert.ok(components.length > 0, 'Should have at least one agent component');
assert.ok(components.some(c => c.id === 'agent:security-reviewer'), 'Should have agent:security-reviewer');
})) passed++; else failed++;
if (test('component catalog includes skill: family entries', () => {
const components = listInstallComponents({ family: 'skill' });
assert.ok(components.length > 0, 'Should have at least one skill component');
assert.ok(components.some(c => c.id === 'skill:continuous-learning'), 'Should have skill:continuous-learning');
})) passed++; else failed++;
// ─── Install Plan Resolution with --with ───
if (test('--with alone resolves component modules and their dependencies', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['lang:typescript'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('framework-language'),
'Should include the module behind lang:typescript');
assert.ok(plan.selectedModuleIds.includes('rules-core'),
'Should include framework-language dependency rules-core');
assert.ok(plan.selectedModuleIds.includes('platform-configs'),
'Should include framework-language dependency platform-configs');
})) passed++; else failed++;
if (test('--with adds modules on top of a profile', () => {
const plan = resolveInstallPlan({
profileId: 'core',
includeComponentIds: ['capability:security'],
target: 'claude',
});
// core profile modules
assert.ok(plan.selectedModuleIds.includes('rules-core'));
assert.ok(plan.selectedModuleIds.includes('workflow-quality'));
// added by --with
assert.ok(plan.selectedModuleIds.includes('security'),
'Should include security module from --with');
})) passed++; else failed++;
if (test('multiple --with flags union their modules', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['lang:typescript', 'capability:database'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('framework-language'),
'Should include framework-language from lang:typescript');
assert.ok(plan.selectedModuleIds.includes('database'),
'Should include database from capability:database');
})) passed++; else failed++;
// ─── Install Plan Resolution with --without ───
if (test('--without excludes modules from a profile', () => {
const plan = resolveInstallPlan({
profileId: 'developer',
excludeComponentIds: ['capability:orchestration'],
target: 'claude',
});
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should exclude orchestration module');
assert.ok(plan.excludedModuleIds.includes('orchestration'),
'Should report orchestration as excluded');
// rest of developer profile should remain
assert.ok(plan.selectedModuleIds.includes('rules-core'));
assert.ok(plan.selectedModuleIds.includes('framework-language'));
assert.ok(plan.selectedModuleIds.includes('database'));
})) passed++; else failed++;
if (test('multiple --without flags exclude multiple modules', () => {
const plan = resolveInstallPlan({
profileId: 'full',
excludeComponentIds: ['capability:media', 'capability:social', 'capability:supply-chain'],
target: 'claude',
});
assert.ok(!plan.selectedModuleIds.includes('media-generation'));
assert.ok(!plan.selectedModuleIds.includes('social-distribution'));
assert.ok(!plan.selectedModuleIds.includes('supply-chain-domain'));
assert.ok(plan.excludedModuleIds.includes('media-generation'));
assert.ok(plan.excludedModuleIds.includes('social-distribution'));
assert.ok(plan.excludedModuleIds.includes('supply-chain-domain'));
})) passed++; else failed++;
// ─── Combined --with + --without ───
if (test('--with and --without work together on a profile', () => {
const plan = resolveInstallPlan({
profileId: 'developer',
includeComponentIds: ['capability:security'],
excludeComponentIds: ['capability:orchestration'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('security'),
'Should include security from --with');
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should exclude orchestration from --without');
assert.ok(plan.selectedModuleIds.includes('rules-core'),
'Should keep profile base modules');
})) passed++; else failed++;
if (test('--without on a dependency of --with raises an error', () => {
assert.throws(
() => resolveInstallPlan({
includeComponentIds: ['capability:social'],
excludeComponentIds: ['capability:content'],
}),
/depends on excluded module/
);
})) passed++; else failed++;
// ─── Validation Errors ───
if (test('throws for unknown component ID in --with', () => {
assert.throws(
() => resolveInstallPlan({
includeComponentIds: ['lang:brainfuck-plus-plus'],
}),
/Unknown install component/
);
})) passed++; else failed++;
if (test('throws for unknown component ID in --without', () => {
assert.throws(
() => resolveInstallPlan({
profileId: 'core',
excludeComponentIds: ['capability:teleportation'],
}),
/Unknown install component/
);
})) passed++; else failed++;
if (test('throws when all modules are excluded', () => {
assert.throws(
() => resolveInstallPlan({
profileId: 'core',
excludeComponentIds: [
'baseline:rules',
'baseline:agents',
'baseline:commands',
'baseline:hooks',
'baseline:platform',
'baseline:workflow',
],
target: 'claude',
}),
/excludes every requested install module/
);
})) passed++; else failed++;
// ─── Target-Specific Behavior ───
if (test('--with respects target compatibility filtering', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['capability:orchestration'],
target: 'cursor',
});
// orchestration module only supports claude, codex, opencode
assert.ok(!plan.selectedModuleIds.includes('orchestration'),
'Should skip orchestration for cursor target');
assert.ok(plan.skippedModuleIds.includes('orchestration'),
'Should report orchestration as skipped for cursor');
})) passed++; else failed++;
if (test('--without with agent: component excludes the agent module', () => {
const plan = resolveInstallPlan({
profileId: 'core',
excludeComponentIds: ['agent:security-reviewer'],
target: 'claude',
});
// agent:security-reviewer maps to agents-core module
// Since core profile includes agents-core and it is excluded, it should be gone
assert.ok(!plan.selectedModuleIds.includes('agents-core'),
'Should exclude agents-core when agent:security-reviewer is excluded');
assert.ok(plan.excludedModuleIds.includes('agents-core'),
'Should report agents-core as excluded');
})) passed++; else failed++;
if (test('--with agent: component includes the agents-core module', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['agent:security-reviewer'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('agents-core'),
'Should include agents-core module from agent:security-reviewer');
})) passed++; else failed++;
if (test('--with skill: component includes the parent skill module', () => {
const plan = resolveInstallPlan({
includeComponentIds: ['skill:continuous-learning'],
target: 'claude',
});
assert.ok(plan.selectedModuleIds.includes('workflow-quality'),
'Should include workflow-quality module from skill:continuous-learning');
})) passed++; else failed++;
// ─── Help Text ───
if (test('help text documents --with and --without flags', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const result = execFileSync('node', [scriptPath, '--help'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('--with'), 'Help should mention --with');
assert.ok(result.includes('--without'), 'Help should mention --without');
assert.ok(result.includes('component'), 'Help should describe components');
})) passed++; else failed++;
// ─── End-to-End Dry-Run ───
if (test('end-to-end: --profile developer --with capability:security --without capability:orchestration --dry-run', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const result = execFileSync('node', [
scriptPath,
'--profile', 'developer',
'--with', 'capability:security',
'--without', 'capability:orchestration',
'--dry-run',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('Mode: manifest'), 'Should be manifest mode');
assert.ok(result.includes('Profile: developer'), 'Should show developer profile');
assert.ok(result.includes('capability:security'), 'Should show included component');
assert.ok(result.includes('capability:orchestration'), 'Should show excluded component');
assert.ok(result.includes('security'), 'Selected modules should include security');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with lang:python --with agent:security-reviewer --dry-run', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const result = execFileSync('node', [
scriptPath,
'--with', 'lang:python',
'--with', 'agent:security-reviewer',
'--dry-run',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
assert.ok(result.includes('Mode: manifest'), 'Should be manifest mode');
assert.ok(result.includes('lang:python'), 'Should show lang:python as included');
assert.ok(result.includes('agent:security-reviewer'), 'Should show agent:security-reviewer as included');
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with with unknown component fails cleanly', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
let exitCode = 0;
let stderr = '';
try {
execFileSync('node', [
scriptPath,
'--with', 'lang:nonexistent-language',
'--dry-run',
], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (error) {
exitCode = error.status || 1;
stderr = error.stderr || '';
}
assert.strictEqual(exitCode, 1, 'Should exit with error code 1');
assert.ok(stderr.includes('Unknown install component'), 'Should report unknown component');
})) passed++; else failed++;
if (test('end-to-end: --without with unknown component fails cleanly', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
let exitCode = 0;
let stderr = '';
try {
execFileSync('node', [
scriptPath,
'--profile', 'core',
'--without', 'capability:nonexistent',
'--dry-run',
], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (error) {
exitCode = error.status || 1;
stderr = error.stderr || '';
}
assert.strictEqual(exitCode, 1, 'Should exit with error code 1');
assert.ok(stderr.includes('Unknown install component'), 'Should report unknown component');
})) passed++; else failed++;
// ─── End-to-End Actual Install ───
if (test('end-to-end: installs --profile core --with capability:security and writes state', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
const result = execFileSync('node', [
scriptPath,
'--profile', 'core',
'--with', 'capability:security',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// Security skill should be installed (from --with)
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'security-review', 'SKILL.md')),
'Should install security-review skill from --with');
// Core profile modules should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install core rules');
// Install state should record include/exclude
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, 'core');
assert.deepStrictEqual(state.request.includeComponents, ['capability:security']);
assert.deepStrictEqual(state.request.excludeComponents, []);
assert.ok(state.resolution.selectedModules.includes('security'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: installs --profile developer --without capability:orchestration and state reflects exclusion', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
execFileSync('node', [
scriptPath,
'--profile', 'developer',
'--without', 'capability:orchestration',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// Orchestration skills should NOT be installed (from --without)
assert.ok(!fs.existsSync(path.join(claudeRoot, 'skills', 'dmux-workflows', 'SKILL.md')),
'Should not install orchestration skills');
// Developer profile base modules should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install core rules');
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')),
'Should install workflow skills');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, 'developer');
assert.deepStrictEqual(state.request.excludeComponents, ['capability:orchestration']);
assert.ok(!state.resolution.selectedModules.includes('orchestration'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('end-to-end: --with alone (no profile) installs just the component modules', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-install-project-'));
try {
execFileSync('node', [
scriptPath,
'--with', 'lang:typescript',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const claudeRoot = path.join(homeDir, '.claude');
// framework-language skill (from lang:typescript) should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md')),
'Should install framework-language skills');
// Its dependencies should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
'Should install dependency rules-core');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
assert.strictEqual(state.request.profile, null);
assert.deepStrictEqual(state.request.includeComponents, ['lang:typescript']);
assert.ok(state.resolution.selectedModules.includes('framework-language'));
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
// ─── JSON output mode ───
if (test('end-to-end: --dry-run --json includes component selections in output', () => {
const { execFileSync } = require('child_process');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'install-apply.js');
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-'));
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'selective-e2e-project-'));
try {
const output = execFileSync('node', [
scriptPath,
'--profile', 'core',
'--with', 'capability:database',
'--without', 'baseline:hooks',
'--dry-run',
'--json',
], {
cwd: projectDir,
env: { ...process.env, HOME: homeDir },
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
const json = JSON.parse(output);
assert.strictEqual(json.dryRun, true);
assert.ok(json.plan, 'Should include plan object');
assert.ok(
json.plan.includedComponentIds.includes('capability:database'),
'JSON output should include capability:database in included components'
);
assert.ok(
json.plan.excludedComponentIds.includes('baseline:hooks'),
'JSON output should include baseline:hooks in excluded components'
);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(projectDir, { recursive: true, force: true });
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();