Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .aiox-core/install-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions bin/aiox-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
},
Expand Down
1 change: 1 addition & 0 deletions packages/installer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
64 changes: 64 additions & 0 deletions packages/installer/src/installer/aiox-core-installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>} 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
Expand Down Expand Up @@ -515,6 +578,7 @@ function sanitizePackageName(name) {
module.exports = {
installAioxCore,
hasPackageJson,
ensureProjectNodeModulesLink,
createBasicPackageJson,
getAioxCoreSourcePath,
copyFileWithRootReplacement,
Expand Down
20 changes: 20 additions & 0 deletions packages/installer/src/pro/pro-scaffolder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -70,6 +71,7 @@ async function scaffoldProContent(targetDir, proSourceDir, options = {}) {
errors: [],
manifest: null,
versionInfo: null,
dependencyResolution: null,
};

// Track files for rollback on partial failure
Expand Down Expand Up @@ -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;
Expand Down
46 changes: 46 additions & 0 deletions tests/installer/aiox-core-installer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const os = require('os');

const {
installAioxCore,
ensureProjectNodeModulesLink,
copyDirectoryWithRootReplacement,
generateFileHashes,
generateVersionJson,
Expand Down Expand Up @@ -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');
});
});
});
26 changes: 26 additions & 0 deletions tests/installer/pro-scaffolder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading