mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-17 23:28:04 +02:00
install-plan.js and install-apply.js both require ./lib/install/config at load time, and that module required ajv unconditionally at the top of the file even though ajv is only actually used when validating an ecc-install.json. When ECC is installed via the Claude Code plugin marketplace, the marketplace directory is a bare git clone with no node_modules, so requiring ajv crashes commands like --list-profiles that never touch install-config validation at all. Same root cause in scripts/lib/control-pane/state.js: sql.js and @iarna/toml were required at module scope even though they are only used inside openSqlDatabase() and readTomlConfig(), so control-pane.js --help crashed too. Make both requires lazy so they only load when the feature that actually needs them runs. For the case where ajv/sql.js/js-yaml/@iarna-toml is genuinely needed and still missing, add a small helper that turns the raw MODULE_NOT_FOUND into an actionable message naming the package and the install command, instead of a stack trace (install-apply.js) or, worse, an unhandled crash with a usage banner tacked on that reads like a bad argument (install-plan.js, control-pane.js). Applied the same helper to memory-mcp.mjs, where ajv is genuinely load-bearing (it compiles every MCP tool's JSON schema up front) so it can't be made lazy the same way. Added a regression test that copies just scripts/, schemas/, and manifests/ into a directory with no node_modules anywhere above it in the filesystem, which reproduces the plugin-marketplace install exactly, and asserts install-plan.js and control-pane.js still work.
92 lines
2.7 KiB
JavaScript
92 lines
2.7 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const DEFAULT_INSTALL_CONFIG = 'ecc-install.json';
|
|
const CONFIG_SCHEMA_PATH = path.join(__dirname, '..', '..', '..', 'schemas', 'ecc-install-config.schema.json');
|
|
|
|
let cachedValidator = null;
|
|
|
|
function readJson(filePath, label) {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
} catch (error) {
|
|
throw new Error(`Invalid JSON in ${label}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function getValidator() {
|
|
if (cachedValidator) {
|
|
return cachedValidator;
|
|
}
|
|
|
|
// ajv is required lazily so scripts that never load an install config (the
|
|
// common case, e.g. `--list-profiles`) don't need it on the require path.
|
|
const Ajv = require('ajv');
|
|
const schema = readJson(CONFIG_SCHEMA_PATH, 'ecc-install-config.schema.json');
|
|
const ajv = new Ajv({ allErrors: true });
|
|
cachedValidator = ajv.compile(schema);
|
|
return cachedValidator;
|
|
}
|
|
|
|
function dedupeStrings(values) {
|
|
return [...new Set((Array.isArray(values) ? values : []).map(value => String(value).trim()).filter(Boolean))];
|
|
}
|
|
|
|
function formatValidationErrors(errors = []) {
|
|
return errors.map(error => `${error.instancePath || '/'} ${error.message}`).join('; ');
|
|
}
|
|
|
|
function resolveInstallConfigPath(configPath, options = {}) {
|
|
if (!configPath) {
|
|
throw new Error('An install config path is required');
|
|
}
|
|
|
|
const cwd = options.cwd || process.cwd();
|
|
return path.isAbsolute(configPath)
|
|
? configPath
|
|
: path.normalize(path.join(cwd, configPath));
|
|
}
|
|
|
|
function findDefaultInstallConfigPath(options = {}) {
|
|
const cwd = options.cwd || process.cwd();
|
|
const candidatePath = path.join(cwd, DEFAULT_INSTALL_CONFIG);
|
|
return fs.existsSync(candidatePath) ? candidatePath : null;
|
|
}
|
|
|
|
function loadInstallConfig(configPath, options = {}) {
|
|
const resolvedPath = resolveInstallConfigPath(configPath, options);
|
|
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
throw new Error(`Install config not found: ${resolvedPath}`);
|
|
}
|
|
|
|
const raw = readJson(resolvedPath, path.basename(resolvedPath));
|
|
const validator = getValidator();
|
|
|
|
if (!validator(raw)) {
|
|
throw new Error(
|
|
`Invalid install config ${resolvedPath}: ${formatValidationErrors(validator.errors)}`
|
|
);
|
|
}
|
|
|
|
return {
|
|
path: resolvedPath,
|
|
version: raw.version,
|
|
target: raw.target || null,
|
|
profileId: raw.profile || null,
|
|
moduleIds: dedupeStrings(raw.modules),
|
|
includeComponentIds: dedupeStrings(raw.include),
|
|
excludeComponentIds: dedupeStrings(raw.exclude),
|
|
options: raw.options && typeof raw.options === 'object' ? { ...raw.options } : {},
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_INSTALL_CONFIG,
|
|
findDefaultInstallConfigPath,
|
|
loadInstallConfig,
|
|
resolveInstallConfigPath,
|
|
};
|