Skip to content

Commit 818c954

Browse files
authored
fix: resolve Pro squad script dependencies (#666)
* fix: resolve pro squad script dependencies (#624) * fix: address squad dependency review feedback (#624)
1 parent 125698c commit 818c954

10 files changed

Lines changed: 228 additions & 5 deletions

File tree

.aiox-core/install-manifest.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# - SHA256 hashes for change detection
88
# - File types for categorization
99
#
10-
version: 5.1.5
11-
generated_at: "2026-05-07T11:58:48.672Z"
10+
version: 5.1.6
11+
generated_at: "2026-05-07T12:43:37.364Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1103
1414
files:

bin/aiox-init.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ try {
110110
brownfieldUpgrader = null;
111111
}
112112

113+
let ensureProjectNodeModulesLink;
114+
try {
115+
({ ensureProjectNodeModulesLink } = require('@aiox-squads/core/installer/aiox-core-installer'));
116+
} catch (_err) {
117+
// Module may not be available in older installations
118+
ensureProjectNodeModulesLink = null;
119+
}
120+
113121
async function main() {
114122
console.clear();
115123

@@ -518,6 +526,24 @@ async function main() {
518526
chalk.gray('(11 agents, 68 tasks, 23 templates)')
519527
);
520528

529+
// Ensure root squad scripts can resolve framework dependencies.
530+
if (ensureProjectNodeModulesLink) {
531+
const linkResult = await ensureProjectNodeModulesLink({
532+
targetDir: context.projectRoot,
533+
targetAioxCore: targetCoreDir,
534+
});
535+
if (linkResult.linked) {
536+
console.log(chalk.green('✓') + ' node_modules linked to .aiox-core/node_modules');
537+
} else if (!linkResult.success) {
538+
console.log(
539+
chalk.yellow('⚠') +
540+
' Could not create node_modules symlink: ' +
541+
linkResult.reason +
542+
(linkResult.error ? ` (${linkResult.error})` : '')
543+
);
544+
}
545+
}
546+
521547
// Create installed manifest for brownfield upgrades (Story 6.18)
522548
if (brownfieldUpgrader) {
523549
try {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# STORY-123.10: Corrigir resolução de dependências dos scripts de squads Pro
2+
3+
Status: Done
4+
5+
PR relacionado: #624
6+
7+
## Contexto
8+
9+
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`.
10+
11+
## Acceptance Criteria
12+
13+
- [x] AC1. Scripts copiados para `squads/` conseguem resolver dependências instaladas em `.aiox-core/node_modules`.
14+
- [x] AC2. O link `node_modules -> .aiox-core/node_modules` só é criado quando o projeto ainda não possui `node_modules`.
15+
- [x] AC3. Projetos com `node_modules` próprio não são sobrescritos.
16+
- [x] AC4. A correção cobre o scaffolder Pro atual e mantém o fallback legado alinhado com o PR #624.
17+
- [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.
18+
19+
## Tasks
20+
21+
- [x] Adicionar helper idempotente de link de dependências no installer core.
22+
- [x] Integrar o helper ao scaffolder Pro.
23+
- [x] Alinhar o fallback legado `bin/aiox-init.js`.
24+
- [x] Adicionar regressões em `tests/installer`.
25+
- [x] Rodar gates locais e preparar patch `5.1.6`.
26+
27+
## Dev Notes
28+
29+
- 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.
30+
- 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.
31+
32+
## File List
33+
34+
- [docs/stories/epic-123/STORY-123.10-squad-dependency-resolution.md](./STORY-123.10-squad-dependency-resolution.md)
35+
- [bin/aiox-init.js](../../../bin/aiox-init.js)
36+
- [packages/installer/src/installer/aiox-core-installer.js](../../../packages/installer/src/installer/aiox-core-installer.js)
37+
- [packages/installer/src/pro/pro-scaffolder.js](../../../packages/installer/src/pro/pro-scaffolder.js)
38+
- [tests/installer/aiox-core-installer.test.js](../../../tests/installer/aiox-core-installer.test.js)
39+
- [tests/installer/pro-scaffolder.test.js](../../../tests/installer/pro-scaffolder.test.js)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aiox-squads/core",
3-
"version": "5.1.5",
3+
"version": "5.1.6",
44
"description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework",
55
"bin": {
66
"aiox": "bin/aiox.js",
@@ -47,6 +47,7 @@
4747
"LICENSE"
4848
],
4949
"exports": {
50+
"./installer/aiox-core-installer": "./packages/installer/src/installer/aiox-core-installer.js",
5051
"./installer/pro-scaffolder": "./packages/installer/src/pro/pro-scaffolder.js",
5152
"./package.json": "./package.json"
5253
},

packages/installer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
},
99
"exports": {
1010
".": "./src/index.js",
11+
"./aiox-core-installer": "./src/installer/aiox-core-installer.js",
1112
"./pro-setup": "./src/wizard/pro-setup.js",
1213
"./pro-scaffolder": "./src/pro/pro-scaffolder.js",
1314
"./package.json": "./package.json"

packages/installer/src/installer/aiox-core-installer.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,69 @@ async function hasPackageJson(targetDir = process.cwd()) {
466466
return fs.pathExists(packageJsonPath);
467467
}
468468

469+
/**
470+
* Link project-level node_modules to the framework dependency install when the
471+
* project has no node_modules of its own. This lets root-level squad scripts
472+
* resolve framework dependencies such as js-yaml after Pro scaffolding.
473+
*
474+
* @param {Object} options - Options
475+
* @param {string} [options.targetDir=process.cwd()] - Project root directory
476+
* @param {string} [options.targetAioxCore] - Installed .aiox-core directory
477+
* @returns {Promise<Object>} Link result
478+
*/
479+
async function ensureProjectNodeModulesLink(options = {}) {
480+
const {
481+
targetDir = process.cwd(),
482+
targetAioxCore = path.join(targetDir, '.aiox-core'),
483+
} = options;
484+
485+
const projectNodeModules = path.join(targetDir, 'node_modules');
486+
const frameworkNodeModules = path.join(targetAioxCore, 'node_modules');
487+
488+
if (await fs.pathExists(projectNodeModules)) {
489+
return {
490+
success: true,
491+
linked: false,
492+
reason: 'project-node-modules-exists',
493+
path: projectNodeModules,
494+
};
495+
}
496+
497+
if (!(await fs.pathExists(frameworkNodeModules))) {
498+
return {
499+
success: false,
500+
linked: false,
501+
reason: 'framework-node-modules-missing',
502+
path: projectNodeModules,
503+
target: frameworkNodeModules,
504+
};
505+
}
506+
507+
const linkTarget = process.platform === 'win32'
508+
? frameworkNodeModules
509+
: path.relative(targetDir, frameworkNodeModules) || frameworkNodeModules;
510+
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
511+
512+
try {
513+
await fs.symlink(linkTarget, projectNodeModules, linkType);
514+
return {
515+
success: true,
516+
linked: true,
517+
path: projectNodeModules,
518+
target: frameworkNodeModules,
519+
};
520+
} catch (error) {
521+
return {
522+
success: false,
523+
linked: false,
524+
reason: 'link-failed',
525+
path: projectNodeModules,
526+
target: frameworkNodeModules,
527+
error: error.message,
528+
};
529+
}
530+
}
531+
469532
/**
470533
* Create a basic package.json for AIOX projects
471534
* @param {Object} options - Options
@@ -515,6 +578,7 @@ function sanitizePackageName(name) {
515578
module.exports = {
516579
installAioxCore,
517580
hasPackageJson,
581+
ensureProjectNodeModulesLink,
518582
createBasicPackageJson,
519583
getAioxCoreSourcePath,
520584
copyFileWithRootReplacement,

packages/installer/src/pro/pro-scaffolder.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const fs = require('fs-extra');
1515
const path = require('path');
1616
const yaml = require('js-yaml');
1717
const { hashFileAsync, hashFilesMatchAsync } = require('../installer/file-hasher');
18+
const { ensureProjectNodeModulesLink } = require('@aiox-squads/installer/aiox-core-installer');
1819

1920
/**
2021
* Directories excluded from scaffolding (private/internal squads).
@@ -70,6 +71,7 @@ async function scaffoldProContent(targetDir, proSourceDir, options = {}) {
7071
errors: [],
7172
manifest: null,
7273
versionInfo: null,
74+
dependencyResolution: null,
7375
};
7476

7577
// Track files for rollback on partial failure
@@ -154,6 +156,24 @@ async function scaffoldProContent(targetDir, proSourceDir, options = {}) {
154156
}
155157
}
156158

159+
const dependencyResolution = await ensureProjectNodeModulesLink({ targetDir });
160+
result.dependencyResolution = dependencyResolution;
161+
if (dependencyResolution.linked) {
162+
rollbackFiles.push(dependencyResolution.path);
163+
}
164+
if (dependencyResolution.linked && onProgress) {
165+
onProgress({
166+
item: 'squad-dependencies',
167+
status: 'done',
168+
message: 'Squad dependency resolution linked',
169+
});
170+
} else if (!dependencyResolution.success) {
171+
result.warnings.push(
172+
`Squad dependency resolution not linked: ${dependencyResolution.reason}` +
173+
(dependencyResolution.error ? ` (${dependencyResolution.error})` : '')
174+
);
175+
}
176+
157177
// Generate pro-version.json (AC4)
158178
const versionInfo = await generateProVersionJson(targetDir, proSourceDir, result.copiedFiles);
159179
result.versionInfo = versionInfo;

tests/installer/aiox-core-installer.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const os = require('os');
1010

1111
const {
1212
installAioxCore,
13+
ensureProjectNodeModulesLink,
1314
copyDirectoryWithRootReplacement,
1415
generateFileHashes,
1516
generateVersionJson,
@@ -222,4 +223,49 @@ describe('AIOX Core Installer - Version Tracking', () => {
222223
expect(await fs.readFile(existingMemoryPath, 'utf8')).toBe('custom project memory');
223224
});
224225
});
226+
227+
describe('ensureProjectNodeModulesLink', () => {
228+
it('should link project node_modules to .aiox-core dependencies when absent', async () => {
229+
const frameworkNodeModules = path.join(tempDir, '.aiox-core', 'node_modules');
230+
await fs.ensureDir(path.join(frameworkNodeModules, 'js-yaml'));
231+
await fs.writeFile(
232+
path.join(frameworkNodeModules, 'js-yaml', 'index.js'),
233+
'module.exports = { ok: true };\n',
234+
);
235+
236+
const result = await ensureProjectNodeModulesLink({ targetDir: tempDir });
237+
238+
expect(result.success).toBe(true);
239+
expect(result.linked).toBe(true);
240+
expect(await fs.pathExists(path.join(tempDir, 'node_modules'))).toBe(true);
241+
expect(await fs.realpath(path.join(tempDir, 'node_modules'))).toBe(
242+
await fs.realpath(frameworkNodeModules),
243+
);
244+
245+
const resolved = require.resolve('js-yaml', {
246+
paths: [path.join(tempDir, 'squads', 'example', 'scripts')],
247+
});
248+
expect(resolved).toContain(path.join('js-yaml', 'index.js'));
249+
});
250+
251+
it('should not overwrite an existing project node_modules directory', async () => {
252+
await fs.ensureDir(path.join(tempDir, 'node_modules', 'existing-package'));
253+
await fs.ensureDir(path.join(tempDir, '.aiox-core', 'node_modules', 'js-yaml'));
254+
255+
const result = await ensureProjectNodeModulesLink({ targetDir: tempDir });
256+
257+
expect(result.success).toBe(true);
258+
expect(result.linked).toBe(false);
259+
expect(result.reason).toBe('project-node-modules-exists');
260+
expect(await fs.pathExists(path.join(tempDir, 'node_modules', 'existing-package'))).toBe(true);
261+
});
262+
263+
it('should report missing .aiox-core dependencies without throwing', async () => {
264+
const result = await ensureProjectNodeModulesLink({ targetDir: tempDir });
265+
266+
expect(result.success).toBe(false);
267+
expect(result.linked).toBe(false);
268+
expect(result.reason).toBe('framework-node-modules-missing');
269+
});
270+
});
225271
});

tests/installer/pro-scaffolder.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,32 @@ describe('scaffoldProContent', () => {
207207
expect(progress.length).toBeGreaterThan(0);
208208
expect(progress.some(p => p.status === 'done')).toBe(true);
209209
});
210+
211+
it('should link framework dependencies so copied squad scripts resolve js-yaml', async () => {
212+
await fs.ensureDir(path.join(targetDir, '.aiox-core', 'node_modules', 'js-yaml'));
213+
await fs.writeFile(
214+
path.join(targetDir, '.aiox-core', 'node_modules', 'js-yaml', 'index.js'),
215+
'module.exports = { ok: true };\n',
216+
);
217+
await fs.ensureDir(path.join(proSourceDir, 'squads', 'devops-squad', 'scripts'));
218+
await fs.writeFile(
219+
path.join(proSourceDir, 'squads', 'devops-squad', 'scripts', 'uses-yaml.js'),
220+
"require('js-yaml');\n",
221+
);
222+
223+
const result = await scaffoldProContent(targetDir, proSourceDir);
224+
225+
expect(result.success).toBe(true);
226+
expect(result.dependencyResolution.linked).toBe(true);
227+
expect(await fs.realpath(path.join(targetDir, 'node_modules'))).toBe(
228+
await fs.realpath(path.join(targetDir, '.aiox-core', 'node_modules')),
229+
);
230+
231+
const resolved = require.resolve('js-yaml', {
232+
paths: [path.join(targetDir, 'squads', 'devops-squad', 'scripts')],
233+
});
234+
expect(resolved).toContain(path.join('js-yaml', 'index.js'));
235+
});
210236
});
211237

212238
describe('rollbackScaffold', () => {

0 commit comments

Comments
 (0)