mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-01 06:23:28 +08:00
feat: add ECC consult command
This commit is contained in:
committed by
Affaan Mustafa
parent
708a8fd715
commit
9a3f72712b
10
README.md
10
README.md
@@ -207,6 +207,16 @@ Add hooks later only if you want runtime enforcement:
|
|||||||
./install.sh --target claude --modules hooks-runtime
|
./install.sh --target claude --modules hooks-runtime
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Find the right components first
|
||||||
|
|
||||||
|
If you are not sure which ECC profile or component to install, ask the packaged advisor from any project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx ecc consult "security reviews" --target claude
|
||||||
|
```
|
||||||
|
|
||||||
|
It returns matching components, related profiles, and preview/install commands. Use the preview command before installing if you want to inspect the exact file plan.
|
||||||
|
|
||||||
### Step 1: Install the Plugin (Recommended)
|
### Step 1: Install the Plugin (Recommended)
|
||||||
|
|
||||||
> NOTE: The plugin is convenient, but the OSS installer below is still the most reliable path if your Claude Code build has trouble resolving self-hosted marketplace entries.
|
> NOTE: The plugin is convenient, but the OSS installer below is still the most reliable path if your Claude Code build has trouble resolving self-hosted marketplace entries.
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
"rules/",
|
"rules/",
|
||||||
"schemas/",
|
"schemas/",
|
||||||
"scripts/catalog.js",
|
"scripts/catalog.js",
|
||||||
|
"scripts/consult.js",
|
||||||
"scripts/auto-update.js",
|
"scripts/auto-update.js",
|
||||||
"scripts/claw.js",
|
"scripts/claw.js",
|
||||||
"scripts/codex/merge-codex-config.js",
|
"scripts/codex/merge-codex-config.js",
|
||||||
|
|||||||
441
scripts/consult.js
Normal file
441
scripts/consult.js
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {
|
||||||
|
SUPPORTED_INSTALL_TARGETS,
|
||||||
|
listInstallComponents,
|
||||||
|
listInstallProfiles,
|
||||||
|
loadInstallManifests,
|
||||||
|
} = require('./lib/install-manifests');
|
||||||
|
|
||||||
|
const DEFAULT_TARGET = 'claude';
|
||||||
|
const DEFAULT_LIMIT = 5;
|
||||||
|
const MAX_LIMIT = 20;
|
||||||
|
const SCHEMA_VERSION = 'ecc.consult.v1';
|
||||||
|
|
||||||
|
const STOP_WORDS = new Set([
|
||||||
|
'a',
|
||||||
|
'an',
|
||||||
|
'and',
|
||||||
|
'app',
|
||||||
|
'are',
|
||||||
|
'for',
|
||||||
|
'from',
|
||||||
|
'i',
|
||||||
|
'in',
|
||||||
|
'into',
|
||||||
|
'me',
|
||||||
|
'need',
|
||||||
|
'of',
|
||||||
|
'on',
|
||||||
|
'please',
|
||||||
|
'skill',
|
||||||
|
'skills',
|
||||||
|
'the',
|
||||||
|
'to',
|
||||||
|
'want',
|
||||||
|
'with',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const COMPONENT_ALIASES = Object.freeze({
|
||||||
|
'capability:security': [
|
||||||
|
'appsec',
|
||||||
|
'auth',
|
||||||
|
'authorization',
|
||||||
|
'checklist',
|
||||||
|
'hardening',
|
||||||
|
'pentest',
|
||||||
|
'secret',
|
||||||
|
'secrets',
|
||||||
|
'threat',
|
||||||
|
'vulnerability',
|
||||||
|
'vulnerabilities',
|
||||||
|
],
|
||||||
|
'capability:database': ['db', 'migration', 'migrations', 'postgres', 'postgresql', 'schema', 'sql'],
|
||||||
|
'capability:research': ['api', 'apis', 'exa', 'external', 'investigation', 'search'],
|
||||||
|
'capability:content': ['article', 'brand', 'business', 'copy', 'linkedin', 'writing'],
|
||||||
|
'capability:operators': ['automation', 'billing', 'connected', 'ops', 'operator', 'workspace'],
|
||||||
|
'capability:social': ['distribution', 'post', 'posting', 'publish', 'publishing', 'twitter', 'x'],
|
||||||
|
'capability:media': ['editing', 'image', 'remotion', 'slides', 'video'],
|
||||||
|
'capability:orchestration': ['dmux', 'parallel', 'tmux', 'worktree', 'worktrees'],
|
||||||
|
'framework:nextjs': ['next', 'next.js', 'nextjs'],
|
||||||
|
'framework:react': ['react', 'tsx'],
|
||||||
|
'framework:django': ['django'],
|
||||||
|
'framework:springboot': ['spring', 'springboot'],
|
||||||
|
'lang:typescript': ['javascript', 'js', 'node', 'nodejs', 'ts'],
|
||||||
|
'lang:python': ['py'],
|
||||||
|
'lang:go': ['golang'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const PROFILE_ALIASES = Object.freeze({
|
||||||
|
minimal: ['low-context', 'lean', 'no-hooks', 'base', 'lightweight'],
|
||||||
|
core: ['baseline', 'default', 'starter'],
|
||||||
|
developer: ['app', 'code', 'coding', 'engineering', 'software'],
|
||||||
|
security: ['appsec', 'audit', 'hardening', 'review', 'threat', 'vulnerability'],
|
||||||
|
research: ['content', 'investigation', 'publishing', 'synthesis'],
|
||||||
|
full: ['all', 'complete', 'everything'],
|
||||||
|
});
|
||||||
|
|
||||||
|
function showHelp(exitCode = 0) {
|
||||||
|
console.log(`
|
||||||
|
Consult ECC install components and profiles from any project
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
node scripts/consult.js "security reviews" [--target <target>] [--limit <n>] [--json]
|
||||||
|
node scripts/consult.js security reviews --target codex
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--target <target> Install target to include in suggested commands. Default: ${DEFAULT_TARGET}
|
||||||
|
--limit <n> Maximum component recommendations to return. Default: ${DEFAULT_LIMIT}
|
||||||
|
--json Emit machine-readable consultation JSON
|
||||||
|
--help Show this help text
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node scripts/consult.js "security reviews"
|
||||||
|
node scripts/consult.js "Next.js React app" --target cursor
|
||||||
|
node scripts/consult.js "operator workflows" --target codex --json
|
||||||
|
`);
|
||||||
|
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeToken(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\.js\b/g, 'js')
|
||||||
|
.replace(/[^a-z0-9:+-]+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandToken(token) {
|
||||||
|
const values = new Set([token]);
|
||||||
|
|
||||||
|
if (token.endsWith('ies') && token.length > 4) {
|
||||||
|
values.add(`${token.slice(0, -3)}y`);
|
||||||
|
}
|
||||||
|
if (token.endsWith('es') && token.length > 4 && !token.endsWith('js')) {
|
||||||
|
values.add(token.slice(0, -2));
|
||||||
|
}
|
||||||
|
if (token.endsWith('s') && token.length > 4 && !token.endsWith('js')) {
|
||||||
|
values.add(token.slice(0, -1));
|
||||||
|
}
|
||||||
|
if (token.endsWith('ing') && token.length > 6) {
|
||||||
|
values.add(token.slice(0, -3));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...values].filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenize(value) {
|
||||||
|
const normalized = normalizeToken(value);
|
||||||
|
if (!normalized) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = [];
|
||||||
|
for (const token of normalized.split(/\s+/)) {
|
||||||
|
if (!token || STOP_WORDS.has(token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tokens.push(...expandToken(token));
|
||||||
|
}
|
||||||
|
return [...new Set(tokens)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePositiveInteger(value, label) {
|
||||||
|
if (!/^[1-9]\d*$/.test(String(value || ''))) {
|
||||||
|
throw new Error(`${label} must be a positive integer`);
|
||||||
|
}
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = argv.slice(2);
|
||||||
|
const parsed = {
|
||||||
|
queryParts: [],
|
||||||
|
target: DEFAULT_TARGET,
|
||||||
|
limit: DEFAULT_LIMIT,
|
||||||
|
json: false,
|
||||||
|
help: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const arg = args[index];
|
||||||
|
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
parsed.help = true;
|
||||||
|
} else if (arg === '--json') {
|
||||||
|
parsed.json = true;
|
||||||
|
} else if (arg === '--target') {
|
||||||
|
if (!args[index + 1]) {
|
||||||
|
throw new Error('Missing value for --target');
|
||||||
|
}
|
||||||
|
parsed.target = args[index + 1];
|
||||||
|
index += 1;
|
||||||
|
} else if (arg === '--limit') {
|
||||||
|
if (!args[index + 1]) {
|
||||||
|
throw new Error('Missing value for --limit');
|
||||||
|
}
|
||||||
|
parsed.limit = Math.min(parsePositiveInteger(args[index + 1], '--limit'), MAX_LIMIT);
|
||||||
|
index += 1;
|
||||||
|
} else if (arg.startsWith('-')) {
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
} else {
|
||||||
|
parsed.queryParts.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SUPPORTED_INSTALL_TARGETS.includes(parsed.target)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unknown install target: ${parsed.target}. Expected one of ${SUPPORTED_INSTALL_TARGETS.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.query = parsed.queryParts.join(' ').trim();
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commandFor(kind, id, target) {
|
||||||
|
if (kind === 'profile') {
|
||||||
|
return `npx ecc install --profile ${id} --target ${target}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `npx ecc install --profile minimal --target ${target} --with ${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function planCommandFor(componentId, target) {
|
||||||
|
return `npx ecc plan --profile minimal --target ${target} --with ${componentId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSearchCorpus(parts) {
|
||||||
|
return tokenize(parts.filter(Boolean).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreAgainstQuery(queryTokens, corpusTokens, options = {}) {
|
||||||
|
const corpus = new Set(corpusTokens);
|
||||||
|
const reasons = [];
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
queryTokens.forEach((token, index) => {
|
||||||
|
if (corpus.has(token)) {
|
||||||
|
score += index === 0 ? 5 : 4;
|
||||||
|
reasons.push(`matched "${token}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
token.length >= 4
|
||||||
|
&& [...corpus].some(corpusToken => (
|
||||||
|
corpusToken.length >= 4
|
||||||
|
&& (corpusToken.includes(token) || token.includes(corpusToken))
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
score += 1;
|
||||||
|
reasons.push(`fuzzy matched "${token}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.preferred && reasons.length > 0) {
|
||||||
|
score += options.preferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { score, reasons: [...new Set(reasons)] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function preferredComponentBonus(component, queryTokens) {
|
||||||
|
let bonus = 0;
|
||||||
|
const suffix = component.id.split(':')[1];
|
||||||
|
|
||||||
|
if (queryTokens[0] === suffix) {
|
||||||
|
bonus += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.family === 'capability') {
|
||||||
|
bonus += 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.id === 'capability:security' && queryTokens.some(token => ['audit', 'review', 'security'].includes(token))) {
|
||||||
|
bonus += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankComponents({ queryTokens, target, limit }) {
|
||||||
|
return listInstallComponents({ target })
|
||||||
|
.map(component => {
|
||||||
|
const aliases = COMPONENT_ALIASES[component.id] || [];
|
||||||
|
const corpusTokens = buildSearchCorpus([
|
||||||
|
component.id.replace(':', ' '),
|
||||||
|
component.family,
|
||||||
|
component.description,
|
||||||
|
component.moduleIds.join(' '),
|
||||||
|
aliases.join(' '),
|
||||||
|
]);
|
||||||
|
const { score, reasons } = scoreAgainstQuery(queryTokens, corpusTokens, {
|
||||||
|
preferred: preferredComponentBonus(component, queryTokens),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
component,
|
||||||
|
score,
|
||||||
|
reasons,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(result => result.score > 0)
|
||||||
|
.sort((left, right) => (
|
||||||
|
right.score - left.score
|
||||||
|
|| left.component.family.localeCompare(right.component.family)
|
||||||
|
|| left.component.id.localeCompare(right.component.id)
|
||||||
|
))
|
||||||
|
.slice(0, limit)
|
||||||
|
.map(result => ({
|
||||||
|
componentId: result.component.id,
|
||||||
|
family: result.component.family,
|
||||||
|
description: result.component.description,
|
||||||
|
moduleIds: result.component.moduleIds,
|
||||||
|
targets: result.component.targets,
|
||||||
|
score: result.score,
|
||||||
|
reasons: result.reasons.length > 0 ? result.reasons : ['related install component'],
|
||||||
|
installCommand: commandFor('component', result.component.id, target),
|
||||||
|
planCommand: planCommandFor(result.component.id, target),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function rankProfiles({ queryTokens, target, limit }) {
|
||||||
|
const manifests = loadInstallManifests();
|
||||||
|
return listInstallProfiles()
|
||||||
|
.map(profile => {
|
||||||
|
const profileDefinition = manifests.profiles[profile.id] || {};
|
||||||
|
const aliases = PROFILE_ALIASES[profile.id] || [];
|
||||||
|
const corpusTokens = buildSearchCorpus([
|
||||||
|
profile.id,
|
||||||
|
profile.description,
|
||||||
|
(profileDefinition.modules || []).join(' '),
|
||||||
|
aliases.join(' '),
|
||||||
|
]);
|
||||||
|
const preferred = queryTokens.includes(profile.id) ? 4 : 0;
|
||||||
|
const { score, reasons } = scoreAgainstQuery(queryTokens, corpusTokens, { preferred });
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
score,
|
||||||
|
reasons,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(result => result.score > 0)
|
||||||
|
.sort((left, right) => right.score - left.score || left.profile.id.localeCompare(right.profile.id))
|
||||||
|
.slice(0, Math.min(3, limit))
|
||||||
|
.map(result => ({
|
||||||
|
id: result.profile.id,
|
||||||
|
description: result.profile.description,
|
||||||
|
moduleCount: result.profile.moduleCount,
|
||||||
|
score: result.score,
|
||||||
|
reasons: result.reasons.length > 0 ? result.reasons : ['related install profile'],
|
||||||
|
installCommand: commandFor('profile', result.profile.id, target),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConsultation(options) {
|
||||||
|
const queryTokens = tokenize(options.query);
|
||||||
|
if (queryTokens.length === 0) {
|
||||||
|
throw new Error('Consult requires a natural language query, for example: security reviews');
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = rankComponents({
|
||||||
|
queryTokens,
|
||||||
|
target: options.target,
|
||||||
|
limit: options.limit,
|
||||||
|
});
|
||||||
|
const profiles = rankProfiles({
|
||||||
|
queryTokens,
|
||||||
|
target: options.target,
|
||||||
|
limit: options.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
schemaVersion: SCHEMA_VERSION,
|
||||||
|
query: options.query,
|
||||||
|
target: options.target,
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
matches,
|
||||||
|
profiles,
|
||||||
|
nextSteps: matches.length > 0
|
||||||
|
? [
|
||||||
|
`Preview the top component: ${matches[0].planCommand}`,
|
||||||
|
`Install it: ${matches[0].installCommand}`,
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'Run `npx ecc catalog components` to browse all components.',
|
||||||
|
'Try a more specific query such as "security review", "Next.js", or "operator workflows".',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatText(payload) {
|
||||||
|
const lines = [
|
||||||
|
`ECC consult (${payload.generatedAt})`,
|
||||||
|
`Query: ${payload.query}`,
|
||||||
|
`Target: ${payload.target}`,
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (payload.matches.length === 0) {
|
||||||
|
lines.push('No strong component matches found.');
|
||||||
|
lines.push('Try: npx ecc catalog components');
|
||||||
|
} else {
|
||||||
|
lines.push('Recommended components:');
|
||||||
|
payload.matches.forEach((match, index) => {
|
||||||
|
lines.push(`${index + 1}. ${match.componentId} [${match.family}]`);
|
||||||
|
lines.push(` ${match.description}`);
|
||||||
|
lines.push(` Install: ${match.installCommand}`);
|
||||||
|
lines.push(` Preview: ${match.planCommand}`);
|
||||||
|
lines.push(` Why: ${match.reasons.join('; ')}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.profiles.length > 0) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Related profiles:');
|
||||||
|
payload.profiles.forEach(profile => {
|
||||||
|
lines.push(`- ${profile.id}: ${profile.description}`);
|
||||||
|
lines.push(` Install: ${profile.installCommand}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Next steps:');
|
||||||
|
payload.nextSteps.forEach(step => lines.push(`- ${step}`));
|
||||||
|
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
try {
|
||||||
|
const options = parseArgs(process.argv);
|
||||||
|
|
||||||
|
if (options.help) {
|
||||||
|
showHelp(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = buildConsultation(options);
|
||||||
|
if (options.json) {
|
||||||
|
console.log(JSON.stringify(payload, null, 2));
|
||||||
|
} else {
|
||||||
|
process.stdout.write(formatText(payload));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildConsultation,
|
||||||
|
formatText,
|
||||||
|
parseArgs,
|
||||||
|
tokenize,
|
||||||
|
};
|
||||||
@@ -17,6 +17,10 @@ const COMMANDS = {
|
|||||||
script: 'catalog.js',
|
script: 'catalog.js',
|
||||||
description: 'Discover install profiles and component IDs',
|
description: 'Discover install profiles and component IDs',
|
||||||
},
|
},
|
||||||
|
consult: {
|
||||||
|
script: 'consult.js',
|
||||||
|
description: 'Recommend ECC components and profiles from a natural language query',
|
||||||
|
},
|
||||||
'install-plan': {
|
'install-plan': {
|
||||||
script: 'install-plan.js',
|
script: 'install-plan.js',
|
||||||
description: 'Alias for plan',
|
description: 'Alias for plan',
|
||||||
@@ -63,6 +67,7 @@ const PRIMARY_COMMANDS = [
|
|||||||
'install',
|
'install',
|
||||||
'plan',
|
'plan',
|
||||||
'catalog',
|
'catalog',
|
||||||
|
'consult',
|
||||||
'list-installed',
|
'list-installed',
|
||||||
'doctor',
|
'doctor',
|
||||||
'repair',
|
'repair',
|
||||||
@@ -97,6 +102,7 @@ Examples:
|
|||||||
ecc catalog profiles
|
ecc catalog profiles
|
||||||
ecc catalog components --family language
|
ecc catalog components --family language
|
||||||
ecc catalog show framework:nextjs
|
ecc catalog show framework:nextjs
|
||||||
|
ecc consult "security reviews"
|
||||||
ecc list-installed --json
|
ecc list-installed --json
|
||||||
ecc doctor --target cursor
|
ecc doctor --target cursor
|
||||||
ecc repair --dry-run
|
ecc repair --dry-run
|
||||||
|
|||||||
115
tests/scripts/consult.test.js
Normal file
115
tests/scripts/consult.test.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* Tests for scripts/consult.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'consult.js');
|
||||||
|
|
||||||
|
function run(args = [], options = {}) {
|
||||||
|
return spawnSync(process.execPath, [SCRIPT, ...args], {
|
||||||
|
cwd: options.cwd || process.cwd(),
|
||||||
|
encoding: 'utf8',
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJson(stdout) {
|
||||||
|
return JSON.parse(stdout.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` PASS ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` FAIL ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests() {
|
||||||
|
console.log('\n=== Testing consult.js ===\n');
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (test('shows help with an explicit help flag', () => {
|
||||||
|
const result = run(['--help']);
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
assert.match(result.stdout, /Consult ECC install components/);
|
||||||
|
assert.match(result.stdout, /node scripts\/consult\.js "security reviews"/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('recommends security components and profile for a natural language query', () => {
|
||||||
|
const result = run(['security', 'reviews', '--json']);
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
const payload = parseJson(result.stdout);
|
||||||
|
assert.strictEqual(payload.schemaVersion, 'ecc.consult.v1');
|
||||||
|
assert.strictEqual(payload.query, 'security reviews');
|
||||||
|
assert.strictEqual(payload.target, 'claude');
|
||||||
|
assert.strictEqual(payload.matches[0].componentId, 'capability:security');
|
||||||
|
assert.ok(payload.matches[0].reasons.some(reason => reason.includes('security')));
|
||||||
|
assert.strictEqual(
|
||||||
|
payload.matches[0].installCommand,
|
||||||
|
'npx ecc install --profile minimal --target claude --with capability:security'
|
||||||
|
);
|
||||||
|
assert.ok(payload.profiles.some(profile => profile.id === 'security'));
|
||||||
|
assert.ok(payload.profiles.find(profile => profile.id === 'security').installCommand.includes('--profile security'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('prints text recommendations with install and plan commands', () => {
|
||||||
|
const result = run(['I', 'want', 'a', 'skill', 'for', 'security', 'reviews']);
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
assert.match(result.stdout, /ECC consult/);
|
||||||
|
assert.match(result.stdout, /capability:security/);
|
||||||
|
assert.match(result.stdout, /npx ecc install --profile minimal --target claude --with capability:security/);
|
||||||
|
assert.match(result.stdout, /npx ecc plan --profile minimal --target claude --with capability:security/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('works from outside the ECC repository', () => {
|
||||||
|
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-consult-project-'));
|
||||||
|
try {
|
||||||
|
const result = run(['nextjs', 'react', '--json'], { cwd: projectDir });
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
const payload = parseJson(result.stdout);
|
||||||
|
assert.strictEqual(payload.matches[0].componentId, 'framework:nextjs');
|
||||||
|
assert.ok(payload.matches.some(match => match.componentId === 'framework:react'));
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(projectDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('filters recommendations by target and limit', () => {
|
||||||
|
const result = run(['operator', 'workflows', '--target', 'codex', '--limit', '1', '--json']);
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
const payload = parseJson(result.stdout);
|
||||||
|
assert.strictEqual(payload.target, 'codex');
|
||||||
|
assert.strictEqual(payload.matches.length, 1);
|
||||||
|
assert.ok(payload.matches[0].targets.includes('codex'));
|
||||||
|
assert.ok(payload.matches[0].installCommand.includes('--target codex'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('rejects unknown targets', () => {
|
||||||
|
const result = run(['security', '--target', 'not-a-target']);
|
||||||
|
|
||||||
|
assert.strictEqual(result.status, 1);
|
||||||
|
assert.match(result.stderr, /Unknown install target/);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
runTests();
|
||||||
@@ -69,6 +69,7 @@ function main() {
|
|||||||
assert.match(result.stdout, /list-installed/);
|
assert.match(result.stdout, /list-installed/);
|
||||||
assert.match(result.stdout, /doctor/);
|
assert.match(result.stdout, /doctor/);
|
||||||
assert.match(result.stdout, /auto-update/);
|
assert.match(result.stdout, /auto-update/);
|
||||||
|
assert.match(result.stdout, /consult/);
|
||||||
assert.match(result.stdout, /loop-status/);
|
assert.match(result.stdout, /loop-status/);
|
||||||
}],
|
}],
|
||||||
['delegates explicit install command', () => {
|
['delegates explicit install command', () => {
|
||||||
@@ -103,6 +104,13 @@ function main() {
|
|||||||
assert.strictEqual(payload.id, 'framework:nextjs');
|
assert.strictEqual(payload.id, 'framework:nextjs');
|
||||||
assert.deepStrictEqual(payload.moduleIds, ['framework-language']);
|
assert.deepStrictEqual(payload.moduleIds, ['framework-language']);
|
||||||
}],
|
}],
|
||||||
|
['delegates consult command', () => {
|
||||||
|
const result = runCli(['consult', 'security', 'reviews', '--json']);
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
const payload = parseJson(result.stdout);
|
||||||
|
assert.strictEqual(payload.schemaVersion, 'ecc.consult.v1');
|
||||||
|
assert.strictEqual(payload.matches[0].componentId, 'capability:security');
|
||||||
|
}],
|
||||||
['delegates lifecycle commands', () => {
|
['delegates lifecycle commands', () => {
|
||||||
const homeDir = createTempDir('ecc-cli-home-');
|
const homeDir = createTempDir('ecc-cli-home-');
|
||||||
const projectRoot = createTempDir('ecc-cli-project-');
|
const projectRoot = createTempDir('ecc-cli-project-');
|
||||||
@@ -188,6 +196,11 @@ function main() {
|
|||||||
assert.strictEqual(result.status, 0, result.stderr);
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
assert.match(result.stdout, /node scripts\/catalog\.js show <component-id>/);
|
assert.match(result.stdout, /node scripts\/catalog\.js show <component-id>/);
|
||||||
}],
|
}],
|
||||||
|
['supports help for the consult subcommand', () => {
|
||||||
|
const result = runCli(['help', 'consult']);
|
||||||
|
assert.strictEqual(result.status, 0, result.stderr);
|
||||||
|
assert.match(result.stdout, /node scripts\/consult\.js "security reviews"/);
|
||||||
|
}],
|
||||||
['fails on unknown commands instead of treating them as installs', () => {
|
['fails on unknown commands instead of treating them as installs', () => {
|
||||||
const result = runCli(['bogus']);
|
const result = runCli(['bogus']);
|
||||||
assert.strictEqual(result.status, 1);
|
assert.strictEqual(result.status, 1);
|
||||||
|
|||||||
@@ -93,6 +93,21 @@ function runTests() {
|
|||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('README documents consult-based component discovery', () => {
|
||||||
|
assert.ok(
|
||||||
|
readme.includes('### Find the right components first'),
|
||||||
|
'README should surface component discovery before install steps'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
readme.includes('npx ecc consult "security reviews" --target claude'),
|
||||||
|
'README should document the packaged consult command'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
readme.includes('It returns matching components, related profiles, and preview/install commands.'),
|
||||||
|
'README should explain what consult returns'
|
||||||
|
);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('README documents Cursor agent namespace and loading caveat', () => {
|
if (test('README documents Cursor agent namespace and loading caveat', () => {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
readme.includes('`.cursor/agents/ecc-*.md`'),
|
readme.includes('`.cursor/agents/ecc-*.md`'),
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ function buildExpectedPublishPaths(repoRoot) {
|
|||||||
"manifests",
|
"manifests",
|
||||||
"scripts/ecc.js",
|
"scripts/ecc.js",
|
||||||
"scripts/catalog.js",
|
"scripts/catalog.js",
|
||||||
|
"scripts/consult.js",
|
||||||
"scripts/claw.js",
|
"scripts/claw.js",
|
||||||
"scripts/doctor.js",
|
"scripts/doctor.js",
|
||||||
"scripts/status.js",
|
"scripts/status.js",
|
||||||
@@ -108,6 +109,7 @@ function main() {
|
|||||||
|
|
||||||
for (const requiredPath of [
|
for (const requiredPath of [
|
||||||
"scripts/catalog.js",
|
"scripts/catalog.js",
|
||||||
|
"scripts/consult.js",
|
||||||
".gemini/GEMINI.md",
|
".gemini/GEMINI.md",
|
||||||
".claude-plugin/plugin.json",
|
".claude-plugin/plugin.json",
|
||||||
".codex-plugin/plugin.json",
|
".codex-plugin/plugin.json",
|
||||||
|
|||||||
Reference in New Issue
Block a user