mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
* feat(install): add guided Claude plugin setup * fix: support Claude command shims on Windows * feat: support safe Claude plugin scope migration * fix(install): preserve interactive setup terminal * fix(install): auto-migrate setup scope changes * feat(install): add guided multi-harness installer * fix(install): sync Yarn binary metadata * fix(install): handle wizard EOF on Node 18 * ci: allow installer matrix tests to finish * test(install): allow slower PowerShell delegation * fix(install): harden guided provider reconciliation * test(install): harden packaged and local compatibility * chore: prepare guided installer release 2.2.0 * fix(install): report refreshed Codex marketplace state * fix(install): verify managed content provenance * test(install): allow empty Yarn smoke fixture * test(install): invoke Windows package shims safely * fix(install): close cross-platform release gaps * fix(install): require trusted GitHub origins * fix(install): preserve hook profile precedence * refactor(install): centralize trusted GitHub origins * ci: retrigger workflow run after merge of main Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
154 lines
4.2 KiB
JavaScript
154 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Shared hook enable/disable controls.
|
|
*
|
|
* Controls:
|
|
* - ECC_HOOKS_ENABLED=true|false (default: true)
|
|
* - ECC_HOOK_PROFILE=minimal|standard|strict (default: standard)
|
|
* - ECC_DISABLED_HOOKS=comma,separated,hook,ids
|
|
*
|
|
* Claude plugin options are used when their corresponding ECC variable is
|
|
* absent. A managed install can provide ecc/setup.json as the final fallback.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const VALID_PROFILES = new Set(['minimal', 'standard', 'strict']);
|
|
|
|
function normalizeId(value) {
|
|
return String(value || '').trim().toLowerCase();
|
|
}
|
|
|
|
function parseBoolean(value, fallback = true) {
|
|
if (value === undefined || value === null || String(value).trim() === '') {
|
|
return fallback;
|
|
}
|
|
const normalized = String(value).trim().toLowerCase();
|
|
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
return fallback;
|
|
}
|
|
|
|
function sanitizeDiagnostic(value) {
|
|
return String(value || '')
|
|
// eslint-disable-next-line no-control-regex
|
|
.replace(/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|\([A-Z]|[A-Z])/g, '')
|
|
.replace(/[^\x20-\x7E]/g, '?');
|
|
}
|
|
|
|
function readManagedHookConfig(env = process.env) {
|
|
const pluginRoot = String(
|
|
env.CLAUDE_PLUGIN_ROOT || env.ECC_PLUGIN_ROOT || ''
|
|
).trim();
|
|
const configPath = String(env.ECC_HOOK_CONFIG || '').trim()
|
|
|| (pluginRoot ? path.join(pluginRoot, 'ecc', 'setup.json') : '');
|
|
if (!configPath || !fs.existsSync(configPath)) return {};
|
|
|
|
try {
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
return config?.hooks
|
|
&& typeof config.hooks === 'object'
|
|
&& !Array.isArray(config.hooks)
|
|
? config.hooks
|
|
: {};
|
|
} catch (error) {
|
|
process.stderr.write(`${sanitizeDiagnostic(
|
|
`Warning: unable to read managed ECC hook config at ${configPath}: ${error.message}`
|
|
)}\n`);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function areHooksEnabled(env = process.env, managed = readManagedHookConfig(env)) {
|
|
const raw = env.ECC_HOOKS_ENABLED !== undefined
|
|
? env.ECC_HOOKS_ENABLED
|
|
: (
|
|
env.CLAUDE_PLUGIN_OPTION_HOOKS_ENABLED !== undefined
|
|
? env.CLAUDE_PLUGIN_OPTION_HOOKS_ENABLED
|
|
: managed.enabled
|
|
);
|
|
return parseBoolean(raw, true);
|
|
}
|
|
|
|
function getHookProfile(env = process.env, managed = readManagedHookConfig(env)) {
|
|
const selected = env.ECC_HOOK_PROFILE !== undefined
|
|
? env.ECC_HOOK_PROFILE
|
|
: (
|
|
env.CLAUDE_PLUGIN_OPTION_HOOK_PROFILE !== undefined
|
|
? env.CLAUDE_PLUGIN_OPTION_HOOK_PROFILE
|
|
: managed.profile
|
|
);
|
|
const raw = String(selected ?? 'standard').trim().toLowerCase();
|
|
return VALID_PROFILES.has(raw) ? raw : 'standard';
|
|
}
|
|
|
|
function getDisabledHookIds(env = process.env) {
|
|
const raw = String(env.ECC_DISABLED_HOOKS || '');
|
|
if (!raw.trim()) return new Set();
|
|
|
|
return new Set(
|
|
raw
|
|
.split(',')
|
|
.map(v => normalizeId(v))
|
|
.filter(Boolean)
|
|
);
|
|
}
|
|
|
|
function parseProfiles(rawProfiles, fallback = ['standard', 'strict']) {
|
|
if (!rawProfiles) return [...fallback];
|
|
|
|
if (Array.isArray(rawProfiles)) {
|
|
const parsed = rawProfiles
|
|
.map(v => String(v || '').trim().toLowerCase())
|
|
.filter(v => VALID_PROFILES.has(v));
|
|
return parsed.length > 0 ? parsed : [...fallback];
|
|
}
|
|
|
|
const parsed = String(rawProfiles)
|
|
.split(',')
|
|
.map(v => v.trim().toLowerCase())
|
|
.filter(v => VALID_PROFILES.has(v));
|
|
|
|
return parsed.length > 0 ? parsed : [...fallback];
|
|
}
|
|
|
|
function isDryRun(env = process.env) {
|
|
return env.ECC_DRY_RUN === '1';
|
|
}
|
|
|
|
function isHookEnabled(hookId, options = {}) {
|
|
const env = options.env || process.env;
|
|
const managed = readManagedHookConfig(env);
|
|
if (!areHooksEnabled(env, managed)) {
|
|
return false;
|
|
}
|
|
|
|
const id = normalizeId(hookId);
|
|
if (!id) return true;
|
|
|
|
const disabled = getDisabledHookIds(env);
|
|
if (disabled.has(id)) {
|
|
return false;
|
|
}
|
|
|
|
const profile = getHookProfile(env, managed);
|
|
const allowedProfiles = parseProfiles(options.profiles);
|
|
return allowedProfiles.includes(profile);
|
|
}
|
|
|
|
module.exports = {
|
|
VALID_PROFILES,
|
|
normalizeId,
|
|
parseBoolean,
|
|
readManagedHookConfig,
|
|
areHooksEnabled,
|
|
getHookProfile,
|
|
getDisabledHookIds,
|
|
parseProfiles,
|
|
isHookEnabled,
|
|
isDryRun,
|
|
};
|