diff --git a/.opencode/README.md b/.opencode/README.md index e239c1361..2d76b7d92 100644 --- a/.opencode/README.md +++ b/.opencode/README.md @@ -241,3 +241,8 @@ selection, and its subagents inherit the invoking primary agent's model. ## License MIT + +Home-target installations keep `plugins/index.ts` inert even with hooks enabled. +OpenCode automatically loads `plugins/ecc-hooks.ts`; loading the barrel as well +would initialize every ECC hook twice. The source and published package barrel +remain available for module imports. diff --git a/scripts/lib/install/hook-consent.js b/scripts/lib/install/hook-consent.js index f954c9016..4876b681e 100644 --- a/scripts/lib/install/hook-consent.js +++ b/scripts/lib/install/hook-consent.js @@ -174,10 +174,6 @@ function withoutOpenCodeHookActivation(operation) { }; } -function transformOpenCodeHookActivationOperations(operations) { - return (Array.isArray(operations) ? operations : []).map(withoutOpenCodeHookActivation); -} - function planSelectsHookRuntime(plan = {}) { return ( Array.isArray(plan.selectedModuleIds) @@ -189,17 +185,26 @@ function planSelectsHookRuntime(plan = {}) { } function disableUnselectedOpenCodeHooks(plan) { - if (plan.target !== 'opencode' || planSelectsHookRuntime(plan)) { + if (plan.target !== 'opencode') { return plan; } + const runtimeSelected = planSelectsHookRuntime(plan); + const transformOperations = operations => (Array.isArray(operations) ? operations : []).map(operation => { + // The source barrel is needed by the published package, but OpenCode scans + // plugins/index.ts and plugins/ecc-hooks.ts independently in home installs. + if (!runtimeSelected || normalizeOperationPath(operation.sourceRelativePath) === '.opencode/plugins/index.ts') { + return withoutOpenCodeHookActivation(operation); + } + return operation; + }); return { ...plan, - operations: transformOpenCodeHookActivationOperations(plan.operations), + operations: transformOperations(plan.operations), statePreview: plan.statePreview ? { ...plan.statePreview, - operations: transformOpenCodeHookActivationOperations(plan.statePreview.operations), + operations: transformOperations(plan.statePreview.operations), } : plan.statePreview, }; diff --git a/tests/lib/hook-consent.test.js b/tests/lib/hook-consent.test.js index d802c5507..9fbade56a 100644 --- a/tests/lib/hook-consent.test.js +++ b/tests/lib/hook-consent.test.js @@ -148,7 +148,9 @@ function runTests() { const pending = withHookConsent(selected, null); assert.throws(() => assertHookConsentReady(pending), /automatic hook runtime/); const enabled = withHookConsent(selected, 'enabled'); - assert.ok(enabled.operations.every(operation => operation.contentTransform === undefined)); + for (const operation of [...enabled.operations, ...enabled.statePreview.operations]) { + assert.strictEqual(operation.contentTransform, operation.sourceRelativePath.endsWith('/index.ts') ? 'opencode-disable-plugin-entrypoint' : undefined); + } assert.doesNotThrow(() => assertHookConsentReady(enabled)); const declined = withHookConsent(selected, 'declined'); assert.strictEqual(declined.operations.length, 2); diff --git a/tests/lib/install-executor.test.js b/tests/lib/install-executor.test.js index d39c14628..1df4889dd 100644 --- a/tests/lib/install-executor.test.js +++ b/tests/lib/install-executor.test.js @@ -990,9 +990,13 @@ function runTests() { for (const name of OPENCODE_ENTRYPOINTS) { assert.strictEqual( fs.readFileSync(path.join(homeDir, '.config', 'opencode', 'plugins', name), 'utf8'), - fs.readFileSync(path.join(REPO_ROOT, '.opencode', 'plugins', name), 'utf8') + name === 'index.ts' ? INERT_OPENCODE_PLUGIN : fs.readFileSync(path.join(REPO_ROOT, '.opencode', 'plugins', name), 'utf8') ); } + assert.strictEqual( + enabledPlan.statePreview.operations.find(operation => operation.sourceRelativePath.split(path.sep).join('/') === '.opencode/plugins/index.ts').contentTransform, + 'opencode-disable-plugin-entrypoint' + ); applyInstallPlanDirect(declinedPlan); const declinedState = JSON.parse(fs.readFileSync(declinedPlan.installStatePath, 'utf8')); assert.strictEqual(declinedState.request.hookConsent, 'declined');