diff --git a/CHANGELOG.md b/CHANGELOG.md index 5993e3634..6c2ff8519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed - **Bazel diagnostics** — `socket manifest bazel --verbose` now emits bounded subprocess traces with argv, cwd, duration, exit status, output sizes, and failure stderr tails to make customer log-only triage safer and faster. +## [1.1.104](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.104) - 2026-05-26 + +### Fixed +- Coana CLI invocation: strip `npm_package_*` env vars before spawning the npm-install fallback. Prevents `spawn E2BIG` failures in large monorepos where the parent process has hundreds of `npm_package_*` env vars populated from the root `package.json`. Preserves `npm_config_*` (registry / proxy / cache from `.npmrc`). + ## [1.1.103](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.103) - 2026-05-26 ### Changed diff --git a/package.json b/package.json index 80cd5aa37..eb82d5031 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.103", + "version": "1.1.104", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT AND OFL-1.1", diff --git a/src/utils/dlx.mts b/src/utils/dlx.mts index e30a6d3ff..4328a0e8c 100644 --- a/src/utils/dlx.mts +++ b/src/utils/dlx.mts @@ -193,6 +193,32 @@ export type CoanaDlxOptions = DlxOptions & { */ const installedCoanaScriptPathsByVersion = new Map() +/** + * Strip npm-injected `npm_package_*` env vars before spawning a Coana + * subprocess. npm (and pnpm/yarn classic) populate one env var per leaf in + * the cwd's package.json — `npm_package_dependencies_*`, `npm_package_scripts_*`, + * etc. In big monorepos with hundreds of deps this can easily account for + * 50KB+ of environment, pushing combined argv + env past Linux ARG_MAX + * (~128KB) and causing `spawn` to fail with E2BIG before Coana even starts. + * + * Coana does not read `npm_package_*` itself, so dropping them is safe. We + * intentionally keep `npm_config_*` (registry, cache, proxy settings sourced + * from .npmrc), `npm_lifecycle_*`, and everything else untouched — those can + * matter for outbound network behavior of nested `npm install` calls. + */ +function sanitizeEnvForCoanaSubprocess( + env: NodeJS.ProcessEnv, +): NodeJS.ProcessEnv { + const out: NodeJS.ProcessEnv = {} + for (const key of Object.keys(env)) { + if (key.startsWith('npm_package_')) { + continue + } + out[key] = env[key] + } + return out +} + /** * Spawn an installed Coana entry point via `node` (or directly, if it's a * native binary). Shared by the SOCKET_CLI_COANA_LOCAL_PATH branch and the @@ -210,7 +236,7 @@ async function spawnCoanaScriptViaNode( const spawnArgs = isBinary ? args : [scriptPath, ...args] const spawnResult = await spawn(isBinary ? scriptPath : 'node', spawnArgs, { cwd: options.cwd, - env: finalEnv, + env: sanitizeEnvForCoanaSubprocess(finalEnv), stdio: spawnExtra?.['stdio'] || 'inherit', }) @@ -278,7 +304,7 @@ async function installCoanaToTmpdir( `@coana-tech/cli@${version}`, ], { - env: finalEnv, + env: sanitizeEnvForCoanaSubprocess(finalEnv), stdio: 'inherit', }, ) diff --git a/src/utils/dlx.test.mts b/src/utils/dlx.test.mts index 44bd32a2a..ce4e59a91 100644 --- a/src/utils/dlx.test.mts +++ b/src/utils/dlx.test.mts @@ -468,5 +468,54 @@ describe('utils/dlx', () => { ) expect(npmInstallCalls).toHaveLength(1) }) + + it('strips npm_package_* env vars in the fallback to avoid E2BIG in big monorepos', async () => { + // Simulate a parent env polluted with npm_package_* (as set by npm/pnpm + // when running inside a project with a populated package.json). The + // fallback must not pass these through to its npm install or node + // spawns, or the same ARG_MAX overflow that broke the dlx path would + // recur. + process.env['npm_package_name'] = 'forge' + process.env['npm_package_dependencies_react'] = '^18.2.0' + process.env['npm_package_devDependencies_typescript'] = '^5.0.0' + // npm_config_* must be preserved — these carry registry/proxy settings + // sourced from .npmrc and are needed for the nested npm install. + process.env['npm_config_registry'] = 'https://artifactory.example/npm/' + + try { + const result = await spawnCoanaDlx(['run', '.'], 'acme', { + coanaVersion: nextVersion(), + }) + expect(result.ok).toBe(true) + + const npmInstallCall = mockSpawn.mock.calls.find( + ([cmd, args]) => cmd === 'npm' && (args as string[])[0] === 'install', + )! + const nodeCall = mockSpawn.mock.calls.find(([cmd]) => cmd === 'node')! + + const npmEnv = (npmInstallCall[2] as { env: NodeJS.ProcessEnv }).env + const nodeEnv = (nodeCall[2] as { env: NodeJS.ProcessEnv }).env + + // npm_package_* are stripped from both spawns. + expect(npmEnv['npm_package_name']).toBeUndefined() + expect(npmEnv['npm_package_dependencies_react']).toBeUndefined() + expect(npmEnv['npm_package_devDependencies_typescript']).toBeUndefined() + expect(nodeEnv['npm_package_name']).toBeUndefined() + expect(nodeEnv['npm_package_dependencies_react']).toBeUndefined() + + // npm_config_* is preserved (registry override survives). + expect(npmEnv['npm_config_registry']).toBe( + 'https://artifactory.example/npm/', + ) + expect(nodeEnv['npm_config_registry']).toBe( + 'https://artifactory.example/npm/', + ) + } finally { + delete process.env['npm_package_name'] + delete process.env['npm_package_dependencies_react'] + delete process.env['npm_package_devDependencies_typescript'] + delete process.env['npm_config_registry'] + } + }) }) })