Skip to content

Commit 40ee7dc

Browse files
committed
fix(reach): honor requested stdio on all Coana launch paths
The dlx branch resolves the caller's requested stdio from both the options and spawnExtra arguments, but spawnCoanaScriptViaNode only read spawnExtra. The local-path, forced npm-install, and auto-mode fallback branches therefore dropped stdio passed via options and defaulted to inherit — `socket fix --silence` requests stdio 'pipe' via options, so those paths leaked Coana output to the terminal. Resolve the requested stdio once and thread it through every launch path.
1 parent db44ab5 commit 40ee7dc

2 files changed

Lines changed: 54 additions & 19 deletions

File tree

src/utils/dlx.mts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ import { isYarnBerry } from './yarn-version.mts'
4242

4343
import type { ShadowBinOptions, ShadowBinResult } from '../shadow/npm-base.mts'
4444
import type { CResult } from '../types.mts'
45-
import type { SpawnExtra } from '@socketsecurity/registry/lib/spawn'
45+
import type {
46+
SpawnExtra,
47+
SpawnOptions,
48+
} from '@socketsecurity/registry/lib/spawn'
4649

4750
const require = createRequire(import.meta.url)
4851

@@ -228,7 +231,10 @@ async function spawnCoanaScriptViaNode(
228231
scriptPath: string,
229232
args: string[] | readonly string[],
230233
finalEnv: NodeJS.ProcessEnv,
231-
options: { cwd?: string | URL | undefined },
234+
options: {
235+
cwd?: string | URL | undefined
236+
stdio?: SpawnOptions['stdio'] | undefined
237+
},
232238
spawnExtra?: SpawnExtra | undefined,
233239
): Promise<CResult<string>> {
234240
const isBinary = !scriptPath.endsWith('.js') && !scriptPath.endsWith('.mjs')
@@ -237,7 +243,7 @@ async function spawnCoanaScriptViaNode(
237243
const spawnResult = await spawn(isBinary ? scriptPath : 'node', spawnArgs, {
238244
cwd: options.cwd,
239245
env: sanitizeEnvForCoanaSubprocess(finalEnv),
240-
stdio: spawnExtra?.['stdio'] || 'inherit',
246+
stdio: options.stdio ?? spawnExtra?.['stdio'] ?? 'inherit',
241247
})
242248

243249
return { ok: true, data: spawnResult.stdout }
@@ -322,7 +328,10 @@ async function spawnCoanaViaNpmInstall(
322328
args: string[] | readonly string[],
323329
version: string,
324330
finalEnv: NodeJS.ProcessEnv,
325-
options: { cwd?: string | URL | undefined },
331+
options: {
332+
cwd?: string | URL | undefined
333+
stdio?: SpawnOptions['stdio'] | undefined
334+
},
326335
spawnExtra?: SpawnExtra | undefined,
327336
): Promise<CResult<string>> {
328337
let scriptPath: string
@@ -454,6 +463,18 @@ export async function spawnCoanaDlx(
454463
const resolvedVersion =
455464
coanaVersion || constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION
456465

466+
// `shadowNpmBase` (the dlx launcher) configures the child's stdio from its
467+
// `options` arg, NOT from the registry-spawn `extra` arg — the latter only
468+
// attaches metadata to the result. Callers that requested streaming via
469+
// `spawnExtra` (the 4th arg), e.g. `{ stdio: 'inherit' }` from
470+
// `socket manifest gradle`, were therefore silently ignored on this path:
471+
// Coana ran piped and its output — including the real failure reason — never
472+
// reached the user, leaving only an unhelpful "command failed". Resolve the
473+
// requested stdio from either argument and honor it on every launch path:
474+
// dlx, local-path, and npm-install (e.g. `socket fix --silence` requests
475+
// `stdio: 'pipe'` via options).
476+
const requestedStdio = spawnExtra?.['stdio'] ?? getOwn(dlxOptions, 'stdio')
477+
457478
const localCoanaPath = process.env['SOCKET_CLI_COANA_LOCAL_PATH']
458479
// Use local Coana CLI if path is provided.
459480
if (localCoanaPath) {
@@ -462,7 +483,7 @@ export async function spawnCoanaDlx(
462483
localCoanaPath,
463484
args,
464485
finalEnv,
465-
{ cwd: dlxOptions.cwd },
486+
{ cwd: dlxOptions.cwd, stdio: requestedStdio },
466487
spawnExtra,
467488
)
468489
} catch (e) {
@@ -479,23 +500,11 @@ export async function spawnCoanaDlx(
479500
args,
480501
resolvedVersion,
481502
finalEnv,
482-
{ cwd: dlxOptions.cwd },
503+
{ cwd: dlxOptions.cwd, stdio: requestedStdio },
483504
spawnExtra,
484505
)
485506
}
486507

487-
// `shadowNpmBase` (the dlx launcher) configures the child's stdio from its
488-
// `options` arg, NOT from the registry-spawn `extra` arg — the latter only
489-
// attaches metadata to the result. Callers that requested streaming via
490-
// `spawnExtra` (the 4th arg), e.g. `{ stdio: 'inherit' }` from
491-
// `socket manifest gradle`, were therefore silently ignored on this path:
492-
// Coana ran piped and its output — including the real failure reason — never
493-
// reached the user, leaving only an unhelpful "command failed". Promote the
494-
// requested stdio into the dlx options so it is honored here too.
495-
// `spawnCoanaScriptViaNode` already reads `spawnExtra.stdio` for the
496-
// local-path and npm-install branches, so this aligns all three paths.
497-
const requestedStdio = spawnExtra?.['stdio'] ?? getOwn(dlxOptions, 'stdio')
498-
499508
try {
500509
// Use npm/dlx version.
501510
const result = await spawnDlx(
@@ -549,7 +558,7 @@ export async function spawnCoanaDlx(
549558
args,
550559
resolvedVersion,
551560
finalEnv,
552-
{ cwd: dlxOptions.cwd },
561+
{ cwd: dlxOptions.cwd, stdio: requestedStdio },
553562
spawnExtra,
554563
)
555564
if (fallbackResult.ok) {

src/utils/dlx.test.mts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,32 @@ describe('utils/dlx', () => {
452452
expect(npmInstallCalls).toHaveLength(1)
453453
})
454454

455+
it('honors options.stdio on the npm-install path', async () => {
456+
process.env['SOCKET_CLI_COANA_LAUNCHER'] = 'npm-install'
457+
458+
const result = await spawnCoanaDlx(['run', '.'], 'acme', {
459+
coanaVersion: nextVersion(),
460+
stdio: 'pipe',
461+
})
462+
463+
expect(result.ok).toBe(true)
464+
const nodeCalls = mockSpawn.mock.calls.filter(([cmd]) => cmd === 'node')
465+
expect(nodeCalls).toHaveLength(1)
466+
expect((nodeCalls[0]![2] as { stdio?: unknown }).stdio).toBe('pipe')
467+
})
468+
469+
it('honors options.stdio in the auto-mode npm-install fallback', async () => {
470+
const result = await spawnCoanaDlx(['run', '.'], 'acme', {
471+
coanaVersion: nextVersion(),
472+
stdio: 'pipe',
473+
})
474+
475+
expect(result.ok).toBe(true)
476+
const nodeCalls = mockSpawn.mock.calls.filter(([cmd]) => cmd === 'node')
477+
expect(nodeCalls).toHaveLength(1)
478+
expect((nodeCalls[0]![2] as { stdio?: unknown }).stdio).toBe('pipe')
479+
})
480+
455481
it('surfaces both dlx and install errors when fallback install fails', async () => {
456482
// Make npm install fail; node would not be reached.
457483
mockSpawn.mockImplementation(async (cmd: string) => {

0 commit comments

Comments
 (0)