Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ npm exec socket
- `SOCKET_CLI_API_BASE_URL` - API base URL (default: `https://api.socket.dev/v0/`)
- `SOCKET_CLI_API_PROXY` - Proxy for API requests (aliases: `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`, `http_proxy`)
- `SOCKET_CLI_API_TIMEOUT` - API request timeout in milliseconds
- `SOCKET_CLI_COANA_LAUNCHER` - How the reachability engine (`@coana-tech/cli`) is launched: `auto` (default; try `npx`, fall back to `npm install` + `node` if the launcher fails), `npx` (never fall back), or `npm-install` (skip `npx` entirely)
- `SOCKET_CLI_DEBUG` - Enable debug logging
- `DEBUG` - Enable [`debug`](https://socket.dev/npm/package/debug) package logging

Expand Down
50 changes: 45 additions & 5 deletions src/utils/dlx.mts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,43 @@ async function spawnCoanaViaNpmInstall(
}
}

type CoanaLauncherMode = 'auto' | 'npm-install' | 'npx'

/**
* Resolve how the Coana engine should be launched.
*
* SOCKET_CLI_COANA_LAUNCHER wins when set:
* - 'auto' (default): try dlx first, fall back to `npm install` + `node` on
* launcher-level failures.
* - 'npm-install': skip dlx entirely; always `npm install` + `node`.
* - 'npx': dlx only; never fall back.
* Unrecognized values warn and behave as 'auto'.
*
* The legacy boolean variables SOCKET_CLI_COANA_FORCE_NPM_INSTALL
* ('npm-install') and SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK ('npx') are still
* honored when the new variable is unset, but are intentionally undocumented.
*/
function getCoanaLauncherMode(): CoanaLauncherMode {
const rawMode = process.env['SOCKET_CLI_COANA_LAUNCHER']
const mode = rawMode?.trim().toLowerCase()
if (mode) {
if (mode === 'auto' || mode === 'npm-install' || mode === 'npx') {
return mode
}
logger.warn(
`Ignoring unrecognized SOCKET_CLI_COANA_LAUNCHER value "${rawMode}"; expected "auto", "npm-install", or "npx".`,
)
return 'auto'
}
if (process.env['SOCKET_CLI_COANA_FORCE_NPM_INSTALL']) {
return 'npm-install'
}
if (process.env['SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK']) {
return 'npx'
}
return 'auto'
}

/**
* Helper to spawn coana with dlx.
* Automatically uses force and silent when version is not pinned exactly.
Expand All @@ -360,9 +397,10 @@ async function spawnCoanaViaNpmInstall(
*
* If the dlx path fails (e.g. broken `npx` on the host), falls back to
* `npm install`-ing @coana-tech/cli into a temp directory and invoking it
* directly via `node`. The fallback can be disabled with
* SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK or forced as the primary path with
* SOCKET_CLI_COANA_FORCE_NPM_INSTALL.
* directly via `node`. The launcher strategy can be overridden with
* SOCKET_CLI_COANA_LAUNCHER: 'auto' (the default) tries dlx with the
* npm-install fallback, 'npm-install' skips dlx entirely, and 'npx' never
* falls back.
*/
export async function spawnCoanaDlx(
args: string[] | readonly string[],
Expand Down Expand Up @@ -432,9 +470,11 @@ export async function spawnCoanaDlx(
}
}

const launcherMode = getCoanaLauncherMode()

// Allow forcing the npm-install path for debugging or for environments
// where dlx is known-broken.
if (process.env['SOCKET_CLI_COANA_FORCE_NPM_INSTALL']) {
if (launcherMode === 'npm-install') {
return await spawnCoanaViaNpmInstall(
args,
resolvedVersion,
Expand Down Expand Up @@ -490,7 +530,7 @@ export async function spawnCoanaDlx(
} catch (e) {
const dlxError = buildDlxErrorResult(e)

if (process.env['SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK']) {
if (launcherMode === 'npx') {
return dlxError
}

Expand Down
70 changes: 70 additions & 0 deletions src/utils/dlx.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ describe('utils/dlx', () => {
beforeEach(async () => {
delete process.env['SOCKET_CLI_COANA_FORCE_NPM_INSTALL']
delete process.env['SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK']
delete process.env['SOCKET_CLI_COANA_LAUNCHER']
delete process.env['SOCKET_CLI_COANA_LOCAL_PATH']

installRoot = await fs.mkdtemp(
Expand Down Expand Up @@ -296,6 +297,7 @@ describe('utils/dlx', () => {
mockSpawn.mockReset()
delete process.env['SOCKET_CLI_COANA_FORCE_NPM_INSTALL']
delete process.env['SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK']
delete process.env['SOCKET_CLI_COANA_LAUNCHER']
await fs.rm(installRoot, { recursive: true, force: true })
})

Expand Down Expand Up @@ -383,6 +385,73 @@ describe('utils/dlx', () => {
expect(npmInstallCalls).toHaveLength(1)
})

it('skips fallback when SOCKET_CLI_COANA_LAUNCHER is npx', async () => {
process.env['SOCKET_CLI_COANA_LAUNCHER'] = 'npx'

const result = await spawnCoanaDlx(['run', '.'], 'acme', {
coanaVersion: nextVersion(),
})

expect(result.ok).toBe(false)
// No npm install was attempted.
const npmInstallCalls = mockSpawn.mock.calls.filter(
([cmd, args]) => cmd === 'npm' && (args as string[])[0] === 'install',
)
expect(npmInstallCalls).toHaveLength(0)
})

it('skips dlx and goes straight to install when SOCKET_CLI_COANA_LAUNCHER is npm-install', async () => {
process.env['SOCKET_CLI_COANA_LAUNCHER'] = 'npm-install'

const result = await spawnCoanaDlx(['run', '.'], 'acme', {
coanaVersion: nextVersion(),
})

expect(result.ok).toBe(true)
// dlx (any shadow bin) was never invoked.
expect(mockDlxBin).not.toHaveBeenCalled()
// npm install ran.
const npmInstallCalls = mockSpawn.mock.calls.filter(
([cmd, args]) => cmd === 'npm' && (args as string[])[0] === 'install',
)
expect(npmInstallCalls).toHaveLength(1)
})

it('prefers SOCKET_CLI_COANA_LAUNCHER over the legacy variables', async () => {
process.env['SOCKET_CLI_COANA_LAUNCHER'] = 'auto'
process.env['SOCKET_CLI_COANA_FORCE_NPM_INSTALL'] = '1'
process.env['SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK'] = '1'

const result = await spawnCoanaDlx(['run', '.'], 'acme', {
coanaVersion: nextVersion(),
})

// The legacy variables are ignored: dlx is still attempted (not forced
// to npm install) and the fallback still runs (not disabled).
expect(result.ok).toBe(true)
expect(mockDlxBin).toHaveBeenCalledTimes(1)
const npmInstallCalls = mockSpawn.mock.calls.filter(
([cmd, args]) => cmd === 'npm' && (args as string[])[0] === 'install',
)
expect(npmInstallCalls).toHaveLength(1)
})

it('treats an unrecognized SOCKET_CLI_COANA_LAUNCHER value as auto', async () => {
process.env['SOCKET_CLI_COANA_LAUNCHER'] = 'bogus'

const result = await spawnCoanaDlx(['run', '.'], 'acme', {
coanaVersion: nextVersion(),
})

// Default behavior: dlx attempted, then the npm-install fallback.
expect(result.ok).toBe(true)
expect(mockDlxBin).toHaveBeenCalledTimes(1)
const npmInstallCalls = mockSpawn.mock.calls.filter(
([cmd, args]) => cmd === 'npm' && (args as string[])[0] === 'install',
)
expect(npmInstallCalls).toHaveLength(1)
})

it('surfaces both dlx and install errors when fallback install fails', async () => {
// Make npm install fail; node would not be reached.
mockSpawn.mockImplementation(async (cmd: string) => {
Expand Down Expand Up @@ -577,6 +646,7 @@ describe('utils/dlx', () => {
beforeEach(() => {
delete process.env['SOCKET_CLI_COANA_FORCE_NPM_INSTALL']
delete process.env['SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK']
delete process.env['SOCKET_CLI_COANA_LAUNCHER']
delete process.env['SOCKET_CLI_COANA_LOCAL_PATH']

// The dlx launcher succeeds by default. spawnDlx picks the shadow bin by
Expand Down