diff --git a/compat/aiox-core/package.json b/compat/aiox-core/package.json index 8599c2cb0b..2a932d64c5 100644 --- a/compat/aiox-core/package.json +++ b/compat/aiox-core/package.json @@ -1,6 +1,6 @@ { "name": "aiox-core", - "version": "5.2.3", + "version": "5.2.4", "description": "Compatibility wrapper for @aiox-squads/core.", "license": "MIT", "bin": { @@ -15,7 +15,7 @@ "README.md" ], "dependencies": { - "@aiox-squads/core": "5.2.3" + "@aiox-squads/core": "5.2.4" }, "engines": { "node": ">=18" diff --git a/package-lock.json b/package-lock.json index cadb1c5cee..b787ee306b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aiox-squads/core", - "version": "5.2.3", + "version": "5.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aiox-squads/core", - "version": "5.2.3", + "version": "5.2.4", "license": "MIT", "workspaces": [ "packages/*" @@ -14542,7 +14542,7 @@ }, "packages/installer": { "name": "@aiox-squads/installer", - "version": "3.3.3", + "version": "3.3.4", "license": "MIT", "dependencies": { "@aiox-squads/core": "^5.1.0", diff --git a/package.json b/package.json index 00768af155..116fbc3867 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aiox-squads/core", - "version": "5.2.3", + "version": "5.2.4", "description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework", "bin": { "aiox": "bin/aiox.js", diff --git a/packages/installer/package.json b/packages/installer/package.json index bbc86a5860..f24308480c 100644 --- a/packages/installer/package.json +++ b/packages/installer/package.json @@ -1,6 +1,6 @@ { "name": "@aiox-squads/installer", - "version": "3.3.3", + "version": "3.3.4", "description": "AIOX Installer - Automated setup wizard for AIOX projects (greenfield & brownfield)", "main": "src/index.js", "bin": { diff --git a/packages/installer/src/wizard/pro-setup.js b/packages/installer/src/wizard/pro-setup.js index 8bdcff758c..b371c9818e 100644 --- a/packages/installer/src/wizard/pro-setup.js +++ b/packages/installer/src/wizard/pro-setup.js @@ -28,6 +28,46 @@ const { t, tf } = require('./i18n'); const execFileAsync = promisify(execFile); +function stripWrappingQuotes(value) { + return String(value || '').trim().replace(/^"(.*)"$/, '$1'); +} + +function resolveNpmInvocation(options = {}) { + const platform = options.platform || process.platform; + const env = options.env || process.env; + const execPath = options.execPath || process.execPath; + const fileExists = options.fileExists || fs.existsSync; + const npmExecPath = stripWrappingQuotes(env.npm_execpath); + + if (npmExecPath && /\.js$/i.test(npmExecPath) && fileExists(npmExecPath)) { + return { + command: execPath, + prefixArgs: [npmExecPath], + execOptions: {}, + }; + } + + return { + command: platform === 'win32' ? 'npm.cmd' : 'npm', + prefixArgs: [], + execOptions: platform === 'win32' ? { shell: true } : {}, + }; +} + +async function runNpm(args, options = {}) { + const invocation = resolveNpmInvocation(); + + return execFileAsync( + invocation.command, + [...invocation.prefixArgs, ...args], + { + ...options, + ...invocation.execOptions, + windowsHide: true, + }, + ); +} + /** * Gold color for Pro branding. * Falls back gracefully if chalk hex is unavailable. @@ -749,10 +789,8 @@ async function extractProArtifactToTemp(artifactPath, tempRoot) { dependencies: {}, }); - const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm'; try { - await execFileAsync( - npmBin, + await runNpm( ['install', artifactPath, '--ignore-scripts', '--no-audit', '--no-fund', '--no-save', '--silent'], { cwd: installRoot, @@ -776,10 +814,8 @@ async function extractProArtifactToTemp(artifactPath, tempRoot) { async function installProArtifactIntoTarget(artifactPath, targetDir) { await fs.ensureDir(targetDir); - const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm'; try { - await execFileAsync( - npmBin, + await runNpm( [ 'install', artifactPath, @@ -2163,6 +2199,7 @@ module.exports = { generateMachineId, persistLicenseCache, resolveLicenseServerUrl, + resolveNpmInvocation, ensureKeyValidationParity, acquireProArtifactSourceDir, downloadArtifactFile, diff --git a/tests/installer/pro-setup-auth.test.js b/tests/installer/pro-setup-auth.test.js index 3c6bd8ffd5..dbc7f06965 100644 --- a/tests/installer/pro-setup-auth.test.js +++ b/tests/installer/pro-setup-auth.test.js @@ -169,6 +169,53 @@ describe('pro-setup backward compatibility (AC-7)', () => { }); }); +describe('pro-setup npm invocation', () => { + it('uses node + npm-cli.js on Windows when npm_execpath is available', () => { + const invocation = proSetup._testing.resolveNpmInvocation({ + platform: 'win32', + execPath: 'C:\\Program Files\\nodejs\\node.exe', + env: { + npm_execpath: 'C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js', + }, + fileExists: () => true, + }); + + expect(invocation).toEqual({ + command: 'C:\\Program Files\\nodejs\\node.exe', + prefixArgs: ['C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js'], + execOptions: {}, + }); + }); + + it('falls back to shell execution for npm.cmd on Windows', () => { + const invocation = proSetup._testing.resolveNpmInvocation({ + platform: 'win32', + env: {}, + fileExists: () => false, + }); + + expect(invocation).toEqual({ + command: 'npm.cmd', + prefixArgs: [], + execOptions: { shell: true }, + }); + }); + + it('uses npm directly on POSIX platforms', () => { + const invocation = proSetup._testing.resolveNpmInvocation({ + platform: 'darwin', + env: {}, + fileExists: () => false, + }); + + expect(invocation).toEqual({ + command: 'npm', + prefixArgs: [], + execOptions: {}, + }); + }); +}); + describe('pro-setup interactive email fallback', () => { afterEach(() => { proSetup._testing.loadLicenseApi = undefined;