diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 9c36232447..ca558521d5 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -7,8 +7,8 @@ # - SHA256 hashes for change detection # - File types for categorization # -version: 5.1.5 -generated_at: "2026-05-07T11:58:48.672Z" +version: 5.1.6 +generated_at: "2026-05-07T12:43:37.364Z" generator: scripts/generate-install-manifest.js file_count: 1103 files: diff --git a/bin/aiox-init.js b/bin/aiox-init.js index a519a69f40..736731bee8 100644 --- a/bin/aiox-init.js +++ b/bin/aiox-init.js @@ -110,6 +110,14 @@ try { brownfieldUpgrader = null; } +let ensureProjectNodeModulesLink; +try { + ({ ensureProjectNodeModulesLink } = require('@aiox-squads/core/installer/aiox-core-installer')); +} catch (_err) { + // Module may not be available in older installations + ensureProjectNodeModulesLink = null; +} + async function main() { console.clear(); @@ -518,6 +526,24 @@ async function main() { chalk.gray('(11 agents, 68 tasks, 23 templates)') ); + // Ensure root squad scripts can resolve framework dependencies. + if (ensureProjectNodeModulesLink) { + const linkResult = await ensureProjectNodeModulesLink({ + targetDir: context.projectRoot, + targetAioxCore: targetCoreDir, + }); + if (linkResult.linked) { + console.log(chalk.green('✓') + ' node_modules linked to .aiox-core/node_modules'); + } else if (!linkResult.success) { + console.log( + chalk.yellow('⚠') + + ' Could not create node_modules symlink: ' + + linkResult.reason + + (linkResult.error ? ` (${linkResult.error})` : '') + ); + } + } + // Create installed manifest for brownfield upgrades (Story 6.18) if (brownfieldUpgrader) { try { diff --git a/docs/stories/epic-123/STORY-123.10-squad-dependency-resolution.md b/docs/stories/epic-123/STORY-123.10-squad-dependency-resolution.md new file mode 100644 index 0000000000..146cc5c487 --- /dev/null +++ b/docs/stories/epic-123/STORY-123.10-squad-dependency-resolution.md @@ -0,0 +1,39 @@ +# STORY-123.10: Corrigir resolução de dependências dos scripts de squads Pro + +Status: Done + +PR relacionado: #624 + +## Contexto + +O PR #624 propôs corrigir scripts em `squads/` que falhavam com `Cannot find module 'js-yaml'` quando executados a partir da raiz do projeto instalado. A proposta original alterava apenas o installer legado (`bin/aiox-init.js`), mas o fluxo atual usa o wizard v4 e copia os squads Pro por `packages/installer/src/pro/pro-scaffolder.js`. + +## Acceptance Criteria + +- [x] AC1. Scripts copiados para `squads/` conseguem resolver dependências instaladas em `.aiox-core/node_modules`. +- [x] AC2. O link `node_modules -> .aiox-core/node_modules` só é criado quando o projeto ainda não possui `node_modules`. +- [x] AC3. Projetos com `node_modules` próprio não são sobrescritos. +- [x] AC4. A correção cobre o scaffolder Pro atual e mantém o fallback legado alinhado com o PR #624. +- [x] AC5. Há testes automatizados para criação do link, idempotência e resolução de `js-yaml` a partir de um script de squad. + +## Tasks + +- [x] Adicionar helper idempotente de link de dependências no installer core. +- [x] Integrar o helper ao scaffolder Pro. +- [x] Alinhar o fallback legado `bin/aiox-init.js`. +- [x] Adicionar regressões em `tests/installer`. +- [x] Rodar gates locais e preparar patch `5.1.6`. + +## Dev Notes + +- O wizard v4 removeu o antigo fluxo comunitário de instalação direta de squads; a superfície ativa com scripts em `squads/` é o conteúdo Pro scaffoldado. +- O link é não destrutivo: se `node_modules` já existir no projeto, a instalação não altera a árvore de dependências do usuário. + +## File List + +- [docs/stories/epic-123/STORY-123.10-squad-dependency-resolution.md](./STORY-123.10-squad-dependency-resolution.md) +- [bin/aiox-init.js](../../../bin/aiox-init.js) +- [packages/installer/src/installer/aiox-core-installer.js](../../../packages/installer/src/installer/aiox-core-installer.js) +- [packages/installer/src/pro/pro-scaffolder.js](../../../packages/installer/src/pro/pro-scaffolder.js) +- [tests/installer/aiox-core-installer.test.js](../../../tests/installer/aiox-core-installer.test.js) +- [tests/installer/pro-scaffolder.test.js](../../../tests/installer/pro-scaffolder.test.js) diff --git a/package-lock.json b/package-lock.json index 6b0d4546fd..efcac54d37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aiox-squads/core", - "version": "5.1.5", + "version": "5.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aiox-squads/core", - "version": "5.1.5", + "version": "5.1.6", "license": "MIT", "workspaces": [ "packages/*" diff --git a/package.json b/package.json index f808df13fd..bb317c2703 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aiox-squads/core", - "version": "5.1.5", + "version": "5.1.6", "description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework", "bin": { "aiox": "bin/aiox.js", @@ -47,6 +47,7 @@ "LICENSE" ], "exports": { + "./installer/aiox-core-installer": "./packages/installer/src/installer/aiox-core-installer.js", "./installer/pro-scaffolder": "./packages/installer/src/pro/pro-scaffolder.js", "./package.json": "./package.json" }, diff --git a/packages/installer/package.json b/packages/installer/package.json index 139fe3cf00..af825c3278 100644 --- a/packages/installer/package.json +++ b/packages/installer/package.json @@ -8,6 +8,7 @@ }, "exports": { ".": "./src/index.js", + "./aiox-core-installer": "./src/installer/aiox-core-installer.js", "./pro-setup": "./src/wizard/pro-setup.js", "./pro-scaffolder": "./src/pro/pro-scaffolder.js", "./package.json": "./package.json" diff --git a/packages/installer/src/installer/aiox-core-installer.js b/packages/installer/src/installer/aiox-core-installer.js index 8a6fd4a617..af94bce6b1 100644 --- a/packages/installer/src/installer/aiox-core-installer.js +++ b/packages/installer/src/installer/aiox-core-installer.js @@ -466,6 +466,69 @@ async function hasPackageJson(targetDir = process.cwd()) { return fs.pathExists(packageJsonPath); } +/** + * Link project-level node_modules to the framework dependency install when the + * project has no node_modules of its own. This lets root-level squad scripts + * resolve framework dependencies such as js-yaml after Pro scaffolding. + * + * @param {Object} options - Options + * @param {string} [options.targetDir=process.cwd()] - Project root directory + * @param {string} [options.targetAioxCore] - Installed .aiox-core directory + * @returns {Promise} Link result + */ +async function ensureProjectNodeModulesLink(options = {}) { + const { + targetDir = process.cwd(), + targetAioxCore = path.join(targetDir, '.aiox-core'), + } = options; + + const projectNodeModules = path.join(targetDir, 'node_modules'); + const frameworkNodeModules = path.join(targetAioxCore, 'node_modules'); + + if (await fs.pathExists(projectNodeModules)) { + return { + success: true, + linked: false, + reason: 'project-node-modules-exists', + path: projectNodeModules, + }; + } + + if (!(await fs.pathExists(frameworkNodeModules))) { + return { + success: false, + linked: false, + reason: 'framework-node-modules-missing', + path: projectNodeModules, + target: frameworkNodeModules, + }; + } + + const linkTarget = process.platform === 'win32' + ? frameworkNodeModules + : path.relative(targetDir, frameworkNodeModules) || frameworkNodeModules; + const linkType = process.platform === 'win32' ? 'junction' : 'dir'; + + try { + await fs.symlink(linkTarget, projectNodeModules, linkType); + return { + success: true, + linked: true, + path: projectNodeModules, + target: frameworkNodeModules, + }; + } catch (error) { + return { + success: false, + linked: false, + reason: 'link-failed', + path: projectNodeModules, + target: frameworkNodeModules, + error: error.message, + }; + } +} + /** * Create a basic package.json for AIOX projects * @param {Object} options - Options @@ -515,6 +578,7 @@ function sanitizePackageName(name) { module.exports = { installAioxCore, hasPackageJson, + ensureProjectNodeModulesLink, createBasicPackageJson, getAioxCoreSourcePath, copyFileWithRootReplacement, diff --git a/packages/installer/src/pro/pro-scaffolder.js b/packages/installer/src/pro/pro-scaffolder.js index d770ee562b..4ec08efd58 100644 --- a/packages/installer/src/pro/pro-scaffolder.js +++ b/packages/installer/src/pro/pro-scaffolder.js @@ -15,6 +15,7 @@ const fs = require('fs-extra'); const path = require('path'); const yaml = require('js-yaml'); const { hashFileAsync, hashFilesMatchAsync } = require('../installer/file-hasher'); +const { ensureProjectNodeModulesLink } = require('@aiox-squads/installer/aiox-core-installer'); /** * Directories excluded from scaffolding (private/internal squads). @@ -70,6 +71,7 @@ async function scaffoldProContent(targetDir, proSourceDir, options = {}) { errors: [], manifest: null, versionInfo: null, + dependencyResolution: null, }; // Track files for rollback on partial failure @@ -154,6 +156,24 @@ async function scaffoldProContent(targetDir, proSourceDir, options = {}) { } } + const dependencyResolution = await ensureProjectNodeModulesLink({ targetDir }); + result.dependencyResolution = dependencyResolution; + if (dependencyResolution.linked) { + rollbackFiles.push(dependencyResolution.path); + } + if (dependencyResolution.linked && onProgress) { + onProgress({ + item: 'squad-dependencies', + status: 'done', + message: 'Squad dependency resolution linked', + }); + } else if (!dependencyResolution.success) { + result.warnings.push( + `Squad dependency resolution not linked: ${dependencyResolution.reason}` + + (dependencyResolution.error ? ` (${dependencyResolution.error})` : '') + ); + } + // Generate pro-version.json (AC4) const versionInfo = await generateProVersionJson(targetDir, proSourceDir, result.copiedFiles); result.versionInfo = versionInfo; diff --git a/tests/installer/aiox-core-installer.test.js b/tests/installer/aiox-core-installer.test.js index 05c3b88e51..f3557cdd7b 100644 --- a/tests/installer/aiox-core-installer.test.js +++ b/tests/installer/aiox-core-installer.test.js @@ -10,6 +10,7 @@ const os = require('os'); const { installAioxCore, + ensureProjectNodeModulesLink, copyDirectoryWithRootReplacement, generateFileHashes, generateVersionJson, @@ -222,4 +223,49 @@ describe('AIOX Core Installer - Version Tracking', () => { expect(await fs.readFile(existingMemoryPath, 'utf8')).toBe('custom project memory'); }); }); + + describe('ensureProjectNodeModulesLink', () => { + it('should link project node_modules to .aiox-core dependencies when absent', async () => { + const frameworkNodeModules = path.join(tempDir, '.aiox-core', 'node_modules'); + await fs.ensureDir(path.join(frameworkNodeModules, 'js-yaml')); + await fs.writeFile( + path.join(frameworkNodeModules, 'js-yaml', 'index.js'), + 'module.exports = { ok: true };\n', + ); + + const result = await ensureProjectNodeModulesLink({ targetDir: tempDir }); + + expect(result.success).toBe(true); + expect(result.linked).toBe(true); + expect(await fs.pathExists(path.join(tempDir, 'node_modules'))).toBe(true); + expect(await fs.realpath(path.join(tempDir, 'node_modules'))).toBe( + await fs.realpath(frameworkNodeModules), + ); + + const resolved = require.resolve('js-yaml', { + paths: [path.join(tempDir, 'squads', 'example', 'scripts')], + }); + expect(resolved).toContain(path.join('js-yaml', 'index.js')); + }); + + it('should not overwrite an existing project node_modules directory', async () => { + await fs.ensureDir(path.join(tempDir, 'node_modules', 'existing-package')); + await fs.ensureDir(path.join(tempDir, '.aiox-core', 'node_modules', 'js-yaml')); + + const result = await ensureProjectNodeModulesLink({ targetDir: tempDir }); + + expect(result.success).toBe(true); + expect(result.linked).toBe(false); + expect(result.reason).toBe('project-node-modules-exists'); + expect(await fs.pathExists(path.join(tempDir, 'node_modules', 'existing-package'))).toBe(true); + }); + + it('should report missing .aiox-core dependencies without throwing', async () => { + const result = await ensureProjectNodeModulesLink({ targetDir: tempDir }); + + expect(result.success).toBe(false); + expect(result.linked).toBe(false); + expect(result.reason).toBe('framework-node-modules-missing'); + }); + }); }); diff --git a/tests/installer/pro-scaffolder.test.js b/tests/installer/pro-scaffolder.test.js index 2a2ace257a..600adf8728 100644 --- a/tests/installer/pro-scaffolder.test.js +++ b/tests/installer/pro-scaffolder.test.js @@ -207,6 +207,32 @@ describe('scaffoldProContent', () => { expect(progress.length).toBeGreaterThan(0); expect(progress.some(p => p.status === 'done')).toBe(true); }); + + it('should link framework dependencies so copied squad scripts resolve js-yaml', async () => { + await fs.ensureDir(path.join(targetDir, '.aiox-core', 'node_modules', 'js-yaml')); + await fs.writeFile( + path.join(targetDir, '.aiox-core', 'node_modules', 'js-yaml', 'index.js'), + 'module.exports = { ok: true };\n', + ); + await fs.ensureDir(path.join(proSourceDir, 'squads', 'devops-squad', 'scripts')); + await fs.writeFile( + path.join(proSourceDir, 'squads', 'devops-squad', 'scripts', 'uses-yaml.js'), + "require('js-yaml');\n", + ); + + const result = await scaffoldProContent(targetDir, proSourceDir); + + expect(result.success).toBe(true); + expect(result.dependencyResolution.linked).toBe(true); + expect(await fs.realpath(path.join(targetDir, 'node_modules'))).toBe( + await fs.realpath(path.join(targetDir, '.aiox-core', 'node_modules')), + ); + + const resolved = require.resolve('js-yaml', { + paths: [path.join(targetDir, 'squads', 'devops-squad', 'scripts')], + }); + expect(resolved).toContain(path.join('js-yaml', 'index.js')); + }); }); describe('rollbackScaffold', () => {