Skip to content

feat(coana): add npm-install + node fallback when dlx fails#1327

Merged
Martin Torp (mtorp) merged 2 commits into
v1.xfrom
martin/invoking-coana-via-npx-fallback
May 22, 2026
Merged

feat(coana): add npm-install + node fallback when dlx fails#1327
Martin Torp (mtorp) merged 2 commits into
v1.xfrom
martin/invoking-coana-via-npx-fallback

Conversation

@mtorp
Copy link
Copy Markdown
Contributor

@mtorp Martin Torp (mtorp) commented May 22, 2026

Summary

  • When npx --yes @coana-tech/cli ... (or the equivalent pnpm dlx / yarn dlx) aborts before Coana itself starts, the Socket CLI now retries by installing @coana-tech/cli into a temp directory via npm install and invoking it directly through node. The registry, npm, and node all work normally in the environments that hit this — only the dlx-driven launch is broken.
  • All three Coana call sites (socket scan reach, socket fix discovery, socket fix per-GHSA loop) inherit the fallback transparently — the change is isolated to spawnCoanaDlx in src/utils/dlx.mts.
  • Installed copies are cached per-version on a module-level Map for the lifetime of the process, so PR mode (which calls Coana once per GHSA) only pays the install cost once.

Env vars

For support/debugging only — not documented in CHANGELOG:

  • SOCKET_CLI_COANA_FORCE_NPM_INSTALL=1 — skip dlx, go straight to the install path. Useful for validating the fallback end-to-end without needing dlx to actually fail.
  • SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK=1 — disable the fallback and restore the previous behavior of returning the dlx error directly.

Test plan

  • pnpm check:tsc — clean
  • pnpm check:lint — clean
  • pnpm test:unit src/utils/dlx.test.mts — 11/11 pass (added 5 new tests covering: fallback fires on dlx error, install caching across calls, DISABLE env skips fallback, FORCE env skips dlx, both errors surface when install also fails)
  • pnpm test:unit for the 4 Coana-caller test files (handle-scan-reach, exclude-paths, handle-create-new-scan, handle-fix-limit) — 47/47 pass
  • Manual smoke: SOCKET_CLI_COANA_FORCE_NPM_INSTALL=1 socket scan reach <project> against a small npm project — observed npm install of @coana-tech/cli@15.3.4 into a socket-coana-* tmpdir, followed by Coana running via node and completing the reachability analysis successfully (.socket.facts.json written, expected vuln counts produced).

Note

Medium Risk
Adds a new execution path that installs and runs @coana-tech/cli via npm install+node, which changes runtime behavior and introduces new failure/perf modes in environments where dlx is flaky.

Overview
spawnCoanaDlx now retries Coana execution when npx/pnpm dlx/yarn dlx fails by installing @coana-tech/cli into a temp dir via npm install and running it via node, caching the resolved CLI entrypoint per version for the lifetime of the process.

Adds support/debug env toggles to force the npm-install path (SOCKET_CLI_COANA_FORCE_NPM_INSTALL) or disable the fallback (SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK), improves error reporting by surfacing both dlx and fallback failures, and adds unit tests covering fallback behavior and caching.

Reviewed by Cursor Bugbot for commit 1549f69. Configure here.

In some CI environments, `npx --yes @coana-tech/cli ...` (or the
pnpm dlx / yarn dlx equivalents) aborts before Coana itself starts
even though the registry, npm, and node all work normally. When that
happens, the Socket CLI now retries by installing @coana-tech/cli into
a temp directory via `npm install` and invoking it directly through
`node`.

Two env vars are exposed for support/debugging:
- SOCKET_CLI_COANA_FORCE_NPM_INSTALL=1 — skip dlx, go straight to the
  install path. Useful for validating the fallback end-to-end.
- SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK=1 — disable the fallback and
  restore the previous behavior of returning the dlx error directly.

The installed Coana is cached per-version for the lifetime of the
process so callers that invoke Coana repeatedly (e.g. `socket fix`
iterating per GHSA in PR mode) only pay the install cost once.
@mtorp Martin Torp (mtorp) marked this pull request as ready for review May 22, 2026 08:11
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Uncaught node spawn errors
    • Wrapped the npm-install fallback Coana node spawn in the existing CResult error conversion and added a regression test for fallback node spawn failures.

Create PR

Or push these changes by commenting:

@cursor push af5c8f716d
Preview (af5c8f716d)
diff --git a/src/utils/dlx.mts b/src/utils/dlx.mts
--- a/src/utils/dlx.mts
+++ b/src/utils/dlx.mts
@@ -316,13 +316,17 @@
       message: `npm install fallback failed: ${stderr || cause}`,
     }
   }
-  return await spawnCoanaScriptViaNode(
-    scriptPath,
-    args,
-    finalEnv,
-    options,
-    spawnExtra,
-  )
+  try {
+    return await spawnCoanaScriptViaNode(
+      scriptPath,
+      args,
+      finalEnv,
+      options,
+      spawnExtra,
+    )
+  } catch (e) {
+    return buildDlxErrorResult(e)
+  }
 }
 
 /**

diff --git a/src/utils/dlx.test.mts b/src/utils/dlx.test.mts
--- a/src/utils/dlx.test.mts
+++ b/src/utils/dlx.test.mts
@@ -388,5 +388,40 @@
       expect(result.message).toContain('npm-install fallback also failed')
       expect(result.message).toContain('registry unreachable')
     })
+
+    it('surfaces both dlx and node errors when fallback node spawn fails', async () => {
+      mockSpawn.mockImplementation(async (cmd: string, args: string[]) => {
+        if (cmd === 'npm' && args[0] === 'install') {
+          const prefixIdx = args.indexOf('--prefix')
+          const installDir = args[prefixIdx + 1]
+          const pkgDir = path.join(
+            installDir,
+            'node_modules',
+            '@coana-tech',
+            'cli',
+          )
+          await fs.mkdir(pkgDir, { recursive: true })
+          await fs.writeFile(
+            path.join(pkgDir, 'package.json'),
+            JSON.stringify({ bin: { coana: 'dist/cli.js' } }),
+          )
+          return { stdout: '', stderr: '' }
+        }
+        throw Object.assign(new Error('node boom'), {
+          code: 127,
+          stderr: 'node failed',
+        })
+      })
+
+      const result = await spawnCoanaDlx(['run', '.'], 'acme', {
+        coanaVersion: nextVersion(),
+      })
+
+      expect(result.ok).toBe(false)
+      expect(result.message).toContain('Coana command failed')
+      expect(result.message).toContain('npx aborted')
+      expect(result.message).toContain('npm-install fallback also failed')
+      expect(result.message).toContain('node failed')
+    })
   })
 })

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1549f69. Configure here.

Comment thread src/utils/dlx.mts Outdated
Comment thread src/utils/dlx.mts
Address two issues with the npm-install + node fallback:

1. spawnCoanaViaNpmInstall now wraps the node spawn in try/catch and
   converts any thrown spawn error into a CResult, mirroring the
   SOCKET_CLI_COANA_LOCAL_PATH branch and the dlx try/catch. Previously a
   Coana process failure (after a successful install) could throw past
   spawnCoanaDlx into callers.

2. Add a shouldFallbackOnDlxError heuristic so we only retry when the
   dlx launcher failed before Coana ran. The previous behavior retried
   on any rejection, which would re-run Coana for real analysis
   failures, wasting time and producing misleading logs. The heuristic:

   - Captured stderr contains "Coana CLI version" → Coana booted, do
     not retry.
   - Spawn-level error (string code like ENOENT) → retry.
   - Killed by signal, or numeric exit code ≥ 128 → retry (covers the
     observed exit 249 / 254 launcher cases).
   - Small integer exit codes with no banner in captured stderr →
     default to NOT retrying (Coana's own exit codes are small
     integers).

Adds four tests covering the new branches: small-integer exit codes
skip the fallback, captured Coana banner skips the fallback,
ENOENT-style errors trigger the fallback, signal kills trigger the
fallback.
@mtorp Martin Torp (mtorp) merged commit 57a3817 into v1.x May 22, 2026
12 of 15 checks passed
@mtorp Martin Torp (mtorp) deleted the martin/invoking-coana-via-npx-fallback branch May 22, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants