mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 04:55:12 +02:00
fix(install): harden universal setup release path (#2888)
* fix(install): harden universal setup release path * docs(adal): use ecc-universal doctor command
This commit is contained in:
@@ -8,6 +8,7 @@ const path = require('path');
|
||||
|
||||
const README = path.join(__dirname, '..', '..', 'README.md');
|
||||
const RULES_README = path.join(__dirname, '..', '..', 'rules', 'README.md');
|
||||
const CODEX_AGENTS = path.join(__dirname, '..', '..', '.codex', 'AGENTS.md');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@@ -29,6 +30,7 @@ function runTests() {
|
||||
|
||||
const readme = fs.readFileSync(README, 'utf8');
|
||||
const rulesReadme = fs.readFileSync(RULES_README, 'utf8');
|
||||
const codexAgents = fs.readFileSync(CODEX_AGENTS, 'utf8');
|
||||
|
||||
if (test('README marks one default path and warns against stacked installs', () => {
|
||||
assert.ok(
|
||||
@@ -50,10 +52,26 @@ function runTests() {
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('README leads with the idempotent guided plugin setup path', () => {
|
||||
const topClaudeSectionIndex = readme.indexOf('## Install with Claude Code');
|
||||
const topGuidedCommandIndex = readme.indexOf('npx ecc-universal setup', topClaudeSectionIndex);
|
||||
const nativePluginCommandIndex = readme.indexOf('/plugin marketplace add', topClaudeSectionIndex);
|
||||
const installSectionIndex = readme.indexOf('## Install ECC');
|
||||
const guidedCommandIndex = readme.indexOf('npx ecc-universal setup', installSectionIndex);
|
||||
const claudeDetailsIndex = readme.indexOf('### Claude Code details', installSectionIndex);
|
||||
|
||||
assert.ok(
|
||||
readme.includes('npx ecc-universal setup'),
|
||||
topGuidedCommandIndex > topClaudeSectionIndex
|
||||
&& topGuidedCommandIndex < nativePluginCommandIndex,
|
||||
'README should lead its public install surface with the canonical package command'
|
||||
);
|
||||
assert.ok(
|
||||
guidedCommandIndex > installSectionIndex,
|
||||
'README should lead new users to the package-name setup command'
|
||||
);
|
||||
assert.ok(
|
||||
guidedCommandIndex < claudeDetailsIndex,
|
||||
'README should show the recommended universal command before provider-specific details'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('installs, updates, or safely moves `ecc@ecc`'),
|
||||
'README should explain that rerunning guided setup reconciles existing installs'
|
||||
@@ -103,6 +121,17 @@ function runTests() {
|
||||
readme.includes('node scripts/ecc.js doctor'),
|
||||
'README should document doctor before reinstalling'
|
||||
);
|
||||
for (const command of [
|
||||
'npx ecc-universal list-installed',
|
||||
'npx ecc-universal doctor',
|
||||
'npx ecc-universal repair',
|
||||
'npx ecc-universal uninstall --dry-run',
|
||||
]) {
|
||||
assert.ok(
|
||||
readme.includes(command),
|
||||
`README should document the package-runner lifecycle command: ${command}`
|
||||
);
|
||||
}
|
||||
assert.ok(
|
||||
readme.includes('ECC only removes files recorded in its install-state.'),
|
||||
'README should explain uninstall safety boundaries'
|
||||
@@ -119,8 +148,12 @@ function runTests() {
|
||||
'README should document the shell minimal profile command'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('npx ecc-install --profile minimal --target claude'),
|
||||
'README should document the npx minimal profile command'
|
||||
readme.includes('npx ecc-universal install --profile minimal --target claude'),
|
||||
'README should document the published universal-package minimal profile command'
|
||||
);
|
||||
assert.ok(
|
||||
!/^\s*npx ecc-install\b/m.test(readme),
|
||||
'README code examples must not invoke the unpublished ecc-install package'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('--profile core --without baseline:hooks --target claude'),
|
||||
@@ -175,6 +208,57 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('README describes the post-release universal install contract', () => {
|
||||
assert.ok(
|
||||
readme.includes('Node.js 18 or newer'),
|
||||
'README should state the runtime required by ecc-universal'
|
||||
);
|
||||
assert.ok(
|
||||
!readme.includes('During registry propagation'),
|
||||
'README should not retain the temporary 2.1 registry fallback after 2.2 is live'
|
||||
);
|
||||
assert.ok(
|
||||
!/codex[^\n]*(?:marketplace|plugin)[^\n]*(?:experimental|unreliable)/i.test(readme),
|
||||
'README should not contradict the supported native Codex install guidance'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('| Skills | Native installed set | Native plugin set |'),
|
||||
'README capability map should describe the native Codex skill set'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('| ECC hooks | Native plugin hooks | Native reviewed subset with explicit trust |'),
|
||||
'README capability map should describe the native Codex hook subset'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes("Codex's narrower native hook set is supplemented"),
|
||||
'README architecture notes should preserve the native Codex hook subset boundary'
|
||||
);
|
||||
assert.ok(
|
||||
!readme.includes("Codex's lack of hooks"),
|
||||
'README should not deny the shipped native Codex hook subset'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes("# Recommended current install: add ECC's native plugin from the repo marketplace"),
|
||||
'README Codex detail should lead with the native plugin install'
|
||||
);
|
||||
assert.ok(
|
||||
readme.includes('Legacy copied-configuration compatibility is still available'),
|
||||
'README Codex detail should label the sync path as compatibility-only'
|
||||
);
|
||||
assert.ok(
|
||||
!readme.includes('# Automatic setup: sync ECC assets'),
|
||||
'README should not present the legacy Codex sync as the primary setup'
|
||||
);
|
||||
assert.ok(
|
||||
codexAgents.includes('Reviewed native subset with explicit trust in `/hooks`'),
|
||||
'Packaged Codex guidance should describe the shipped trusted hook subset'
|
||||
);
|
||||
assert.ok(
|
||||
!/not yet supported|codex lacks hooks|security without hooks/i.test(codexAgents),
|
||||
'Packaged Codex guidance should not deny native hook support'
|
||||
);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('README documents Cursor agent namespace and loading caveat', () => {
|
||||
assert.ok(
|
||||
readme.includes('`.cursor/agents/ecc-*.md`'),
|
||||
|
||||
+103
-10
@@ -56,18 +56,21 @@ function createFixture(state = {}) {
|
||||
callsPath,
|
||||
};
|
||||
}
|
||||
function runSetup(fixture, args) {
|
||||
function runSetup(fixture, args, options = {}) {
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: fixture.homeDir,
|
||||
USERPROFILE: fixture.homeDir,
|
||||
CLAUDE_CONFIG_DIR: fixture.configDir,
|
||||
PATH: options.path || `${fixture.binDir}${path.delimiter}${process.env.PATH || ''}`,
|
||||
ECC_TEST_CLAUDE_STATE: fixture.statePath,
|
||||
ECC_TEST_CLAUDE_CALLS: fixture.callsPath,
|
||||
...options.env,
|
||||
};
|
||||
if (options.defaultClaudeConfig) delete env.CLAUDE_CONFIG_DIR;
|
||||
return spawnSync(process.execPath, [setupScript, ...args], {
|
||||
cwd: fixture.projectRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: fixture.homeDir,
|
||||
USERPROFILE: fixture.homeDir,
|
||||
CLAUDE_CONFIG_DIR: fixture.configDir,
|
||||
PATH: `${fixture.binDir}${path.delimiter}${process.env.PATH || ''}`,
|
||||
ECC_TEST_CLAUDE_STATE: fixture.statePath,
|
||||
ECC_TEST_CLAUDE_CALLS: fixture.callsPath,
|
||||
},
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
timeout: 15000,
|
||||
});
|
||||
@@ -267,6 +270,96 @@ test('dry-run JSON emits JSON only and reads inventory without mutation', () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('dry-run leaves a pristine HOME unchanged when Claude inventory creates backups', () => {
|
||||
withFixture({}, fixture => {
|
||||
assert.deepStrictEqual(fs.readdirSync(fixture.homeDir), []);
|
||||
assert.deepStrictEqual(fs.readdirSync(fixture.projectRoot), []);
|
||||
const result = runSetup(fixture, [
|
||||
'--mode', 'claude-plugin',
|
||||
'--scope', 'local',
|
||||
'--hooks', 'minimal',
|
||||
'--dry-run',
|
||||
'--json',
|
||||
], {
|
||||
defaultClaudeConfig: true,
|
||||
env: { ECC_TEST_CLAUDE_CREATE_READ_ARTIFACTS: '1' },
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.deepStrictEqual(fs.readdirSync(fixture.homeDir), []);
|
||||
assert.deepStrictEqual(fs.readdirSync(fixture.projectRoot), []);
|
||||
});
|
||||
});
|
||||
|
||||
test('dry-run isolates pre-existing Claude backups and symlinked project settings', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
withFixture({}, fixture => {
|
||||
const defaultConfigDir = path.join(fixture.homeDir, '.claude');
|
||||
const backupDir = path.join(defaultConfigDir, 'backups');
|
||||
const backupPath = path.join(backupDir, 'existing.backup');
|
||||
const statePath = path.join(fixture.homeDir, '.claude.json');
|
||||
const projectConfigDir = path.join(fixture.projectRoot, '.claude');
|
||||
const settingsTarget = path.join(fixture.root, 'settings-target.json');
|
||||
const settingsPath = path.join(projectConfigDir, 'settings.local.json');
|
||||
const xdgDataHome = path.join(fixture.homeDir, 'xdg-data');
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
fs.mkdirSync(projectConfigDir, { recursive: true });
|
||||
fs.writeFileSync(backupPath, 'original-backup\n');
|
||||
fs.writeFileSync(statePath, '{"original":true}\n');
|
||||
fs.writeFileSync(settingsTarget, '{"enabledPlugins":{"ecc@ecc":true}}\n');
|
||||
fs.symlinkSync(settingsTarget, settingsPath, 'file');
|
||||
|
||||
const result = runSetup(fixture, [
|
||||
'--mode', 'claude-plugin',
|
||||
'--scope', 'local',
|
||||
'--hooks', 'minimal',
|
||||
'--dry-run',
|
||||
'--json',
|
||||
], {
|
||||
defaultClaudeConfig: true,
|
||||
env: {
|
||||
ECC_TEST_CLAUDE_CREATE_READ_ARTIFACTS: '1',
|
||||
ECC_TEST_CLAUDE_OVERWRITE_READ_ARTIFACTS: '1',
|
||||
ECC_TEST_CLAUDE_WRITE_XDG_DATA: '1',
|
||||
XDG_DATA_HOME: xdgDataHome,
|
||||
},
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.strictEqual(fs.readFileSync(backupPath, 'utf8'), 'original-backup\n');
|
||||
assert.strictEqual(fs.readFileSync(statePath, 'utf8'), '{"original":true}\n');
|
||||
assert.strictEqual(
|
||||
fs.readFileSync(settingsTarget, 'utf8'),
|
||||
'{"enabledPlugins":{"ecc@ecc":true}}\n'
|
||||
);
|
||||
assert.strictEqual(fs.lstatSync(settingsPath).isSymbolicLink(), true);
|
||||
assert.strictEqual(
|
||||
fs.existsSync(path.join(xdgDataHome, 'claude-provider-read.json')),
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('missing Git fails with an actionable prerequisite during dry-run', () => {
|
||||
withFixture({}, fixture => {
|
||||
const result = runSetup(fixture, [
|
||||
'--mode', 'claude-plugin',
|
||||
'--scope', 'user',
|
||||
'--hooks', 'standard',
|
||||
'--dry-run',
|
||||
'--json',
|
||||
], { path: fixture.binDir });
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.strictEqual(result.stdout, '');
|
||||
const payload = JSON.parse(result.stderr);
|
||||
assert.strictEqual(payload.error.code, 'GIT_NOT_FOUND');
|
||||
assert.strictEqual(payload.error.phase, 'preflight');
|
||||
assert.match(payload.error.message, /Git is required for Claude marketplace setup/i);
|
||||
assert.match(payload.error.message, /install Git/i);
|
||||
assert.doesNotMatch(payload.error.message, /ERR_STREAM_PREMATURE_CLOSE/i);
|
||||
assert.deepStrictEqual(readCalls(fixture), []);
|
||||
});
|
||||
});
|
||||
|
||||
test('setup automatically migrates an existing install to the selected scope and hooks', () => {
|
||||
withFixture({
|
||||
plugins: [{ id: 'ecc@ecc', scope: 'local', enabled: true, version: '1.9.0' }],
|
||||
|
||||
Reference in New Issue
Block a user