From 3990afedb7f381c9622f7b329957465a3e9d2d43 Mon Sep 17 00:00:00 2001 From: Nikolas de Hor Date: Fri, 13 Mar 2026 00:34:35 -0300 Subject: [PATCH] fix(installer): instala hooks condicionalmente por tier free/pro (#544) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separa hooks em HOOKS_FREE e HOOKS_PRO_ONLY. O precompact-session-digest.cjs só é copiado quando pro/ está disponível (detectado via pro-detector). Evita processo desnecessário em todo compact para usuários free. --- .aiox-core/install-manifest.yaml | 2 +- .../src/wizard/ide-config-generator.js | 33 ++++++-- tests/installer/conditional-hooks.test.js | 79 +++++++++++++++++++ 3 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 tests/installer/conditional-hooks.test.js diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 05816fa4f..f5332645d 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.0.3 -generated_at: "2026-03-11T15:04:09.395Z" +generated_at: "2026-03-13T03:34:29.010Z" generator: scripts/generate-install-manifest.js file_count: 1090 files: diff --git a/packages/installer/src/wizard/ide-config-generator.js b/packages/installer/src/wizard/ide-config-generator.js index 701a3a10b..0d0025d86 100644 --- a/packages/installer/src/wizard/ide-config-generator.js +++ b/packages/installer/src/wizard/ide-config-generator.js @@ -652,9 +652,29 @@ function showSuccessSummary(result) { console.log(' 4. Use * commands to interact with agents\n'); } +/** + * Hooks disponíveis para todos os tiers (free + pro) + * @type {string[]} + */ +const HOOKS_FREE = [ + 'synapse-engine.cjs', + 'synapse-wrapper.cjs', + 'precompact-wrapper.cjs', + 'README.md', +]; + +/** + * Hooks exclusivos do tier pro (requerem aios-pro) + * @type {string[]} + */ +const HOOKS_PRO_ONLY = [ + 'precompact-session-digest.cjs', +]; + /** * BUG-3 fix (INS-1): Copy .claude/hooks/ folder during installation * Only copies JS hooks that work without external dependencies (Python, etc.) + * Pro-only hooks (precompact-session-digest) are skipped when pro/ is unavailable (#544) * @param {string} projectRoot - Project root directory * @returns {Promise} List of copied files */ @@ -674,13 +694,12 @@ async function copyClaudeHooksFolder(projectRoot) { await fs.ensureDir(targetDir); - // Only copy JS hooks that work standalone (no Python/shell deps) - const HOOKS_TO_COPY = [ - 'synapse-engine.cjs', - 'code-intel-pretool.cjs', - 'precompact-session-digest.cjs', - 'README.md', - ]; + // Detecta se pro/ está disponível para decidir quais hooks copiar (#544) + const { isProAvailable } = require('../../../../bin/utils/pro-detector'); + const isPro = isProAvailable(); + const HOOKS_TO_COPY = isPro + ? [...HOOKS_FREE, ...HOOKS_PRO_ONLY] + : HOOKS_FREE; const files = await fs.readdir(sourceDir); diff --git a/tests/installer/conditional-hooks.test.js b/tests/installer/conditional-hooks.test.js new file mode 100644 index 000000000..24cbab987 --- /dev/null +++ b/tests/installer/conditional-hooks.test.js @@ -0,0 +1,79 @@ +/** + * Testes para instalação condicional de hooks por tier (#544) + * + * Valida que hooks pro-only não são copiados quando pro/ não está disponível. + * + * @see packages/installer/src/wizard/ide-config-generator.js + * @issue #544 + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs-extra'); +const os = require('os'); + +// O módulo sob teste +const { + copyClaudeHooksFolder, +} = require('../../packages/installer/src/wizard/ide-config-generator'); + +// Mock do pro-detector +jest.mock('../../bin/utils/pro-detector'); +const proDetector = require('../../bin/utils/pro-detector'); + +function makeTempDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-test-')); +} + +describe('copyClaudeHooksFolder — tier-aware (#544)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = makeTempDir(); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('deve copiar apenas hooks free quando pro não está disponível', async () => { + proDetector.isProAvailable.mockReturnValue(false); + + const copiedFiles = await copyClaudeHooksFolder(tmpDir); + const fileNames = copiedFiles.map((f) => path.basename(f)); + + expect(fileNames).toContain('synapse-engine.cjs'); + expect(fileNames).toContain('README.md'); + expect(fileNames).not.toContain('precompact-session-digest.cjs'); + }); + + it('deve copiar todos os hooks incluindo pro quando pro está disponível', async () => { + proDetector.isProAvailable.mockReturnValue(true); + + const copiedFiles = await copyClaudeHooksFolder(tmpDir); + const fileNames = copiedFiles.map((f) => path.basename(f)); + + expect(fileNames).toContain('synapse-engine.cjs'); + expect(fileNames).toContain('precompact-session-digest.cjs'); + expect(fileNames).toContain('README.md'); + }); + + it('deve retornar array vazio quando source === target (framework-dev)', async () => { + proDetector.isProAvailable.mockReturnValue(false); + + // Aponta projectRoot para o próprio framework root + const frameworkRoot = path.resolve(__dirname, '..', '..'); + const result = await copyClaudeHooksFolder(frameworkRoot); + expect(result).toEqual([]); + }); + + it('deve sempre copiar README.md independente do tier', async () => { + proDetector.isProAvailable.mockReturnValue(false); + + const copiedFiles = await copyClaudeHooksFolder(tmpDir); + const fileNames = copiedFiles.map((f) => path.basename(f)); + + expect(fileNames).toContain('README.md'); + }); +});