Skip to content

Commit 829e53b

Browse files
committed
Merge remote-tracking branch 'origin/v1.x' into coana-15.3.22
# Conflicts: # CHANGELOG.md
2 parents 25d420a + 4c02365 commit 829e53b

6 files changed

Lines changed: 336 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7-
## [1.1.115](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.115) - 2026-06-06
7+
## [1.1.116](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.116) - 2026-06-06
88

99
### Changed
1010
- Updated the Coana CLI to v `15.3.22`.
1111

12+
## [1.1.115](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.115) - 2026-06-04
13+
14+
### Fixed
15+
- `socket manifest gradle`, `kotlin`, and `scala` (including sbt-based projects) now stream the underlying build-tool and Coana output and surface the real failure reason. Previously a generation failure could collapse to an unhelpful `Coana command failed (exit code 1): command failed` with no detail, hiding actionable hints such as unresolved dependencies (re-run with `--ignore-unresolved` / `--exclude-configs`, or `--pom` for the legacy `pom.xml` output).
16+
1217
## [1.1.114](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.114) - 2026-06-04
1318

1419
### Changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "socket",
3-
"version": "1.1.115",
3+
"version": "1.1.116",
44
"description": "CLI for Socket.dev",
55
"homepage": "https://github.com/SocketDev/socket-cli",
66
"license": "MIT AND OFL-1.1",

src/commands/ci/fetch-default-org-slug.mts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export async function getDefaultOrgSlug(
4545
}
4646
}
4747

48-
const slug = (organizations as any)[keys[0]!]?.name ?? undefined
48+
// Use the org's URL-safe `slug`, not its display `name`: this value is
49+
// exported as SOCKET_ORG_SLUG for the Coana CLI, which resolves the org by
50+
// slug. `name` is the human-readable display name (and may be null), so using
51+
// it here produced a wrong/empty org identifier.
52+
const slug = organizations[0]?.slug ?? undefined
4953
if (!slug) {
5054
return {
5155
ok: false,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
import { getDefaultOrgSlug } from './fetch-default-org-slug.mts'
4+
import { fetchOrganization } from '../organization/fetch-organization-list.mts'
5+
import { getConfigValueOrUndef } from '../../utils/config.mts'
6+
7+
vi.mock('../organization/fetch-organization-list.mts', () => ({
8+
fetchOrganization: vi.fn(),
9+
}))
10+
vi.mock('../../utils/config.mts', () => ({
11+
getConfigValueOrUndef: vi.fn(() => undefined),
12+
}))
13+
// Keep SOCKET_CLI_ORG_SLUG unset so the resolver falls through to the API path.
14+
vi.mock('../../constants.mts', () => ({
15+
default: { ENV: {} },
16+
}))
17+
18+
describe('getDefaultOrgSlug', () => {
19+
beforeEach(() => {
20+
vi.clearAllMocks()
21+
vi.mocked(getConfigValueOrUndef).mockReturnValue(undefined)
22+
})
23+
24+
it('resolves the org slug (not the display name) from the API', async () => {
25+
vi.mocked(fetchOrganization).mockResolvedValue({
26+
ok: true,
27+
data: {
28+
organizations: [
29+
{
30+
id: 'org-id',
31+
name: 'Display Name',
32+
image: null,
33+
plan: 'free',
34+
slug: 'my-org-slug',
35+
},
36+
],
37+
},
38+
} as any)
39+
40+
const result = await getDefaultOrgSlug()
41+
42+
expect(result.ok).toBe(true)
43+
// Regression guard: must be the URL-safe slug, never the display name.
44+
expect(result.ok && result.data).toBe('my-org-slug')
45+
})
46+
47+
it('resolves the slug even when the display name is null', async () => {
48+
vi.mocked(fetchOrganization).mockResolvedValue({
49+
ok: true,
50+
data: {
51+
organizations: [
52+
{
53+
id: 'org-id',
54+
name: null,
55+
image: null,
56+
plan: 'free',
57+
slug: 'slug-only',
58+
},
59+
],
60+
},
61+
} as any)
62+
63+
const result = await getDefaultOrgSlug()
64+
65+
expect(result.ok).toBe(true)
66+
expect(result.ok && result.data).toBe('slug-only')
67+
})
68+
69+
it('prefers the defaultOrg config value without calling the API', async () => {
70+
vi.mocked(getConfigValueOrUndef).mockReturnValue('configured-org')
71+
72+
const result = await getDefaultOrgSlug()
73+
74+
expect(result.ok).toBe(true)
75+
expect(result.ok && result.data).toBe('configured-org')
76+
expect(fetchOrganization).not.toHaveBeenCalled()
77+
})
78+
79+
it('fails when the API returns no organizations', async () => {
80+
vi.mocked(fetchOrganization).mockResolvedValue({
81+
ok: true,
82+
data: { organizations: [] },
83+
} as any)
84+
85+
const result = await getDefaultOrgSlug()
86+
87+
expect(result.ok).toBe(false)
88+
})
89+
})

src/utils/dlx.mts

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,18 @@ export async function spawnCoanaDlx(
444444
)
445445
}
446446

447+
// `shadowNpmBase` (the dlx launcher) configures the child's stdio from its
448+
// `options` arg, NOT from the registry-spawn `extra` arg — the latter only
449+
// attaches metadata to the result. Callers that requested streaming via
450+
// `spawnExtra` (the 4th arg), e.g. `{ stdio: 'inherit' }` from
451+
// `socket manifest gradle`, were therefore silently ignored on this path:
452+
// Coana ran piped and its output — including the real failure reason — never
453+
// reached the user, leaving only an unhelpful "command failed". Promote the
454+
// requested stdio into the dlx options so it is honored here too.
455+
// `spawnCoanaScriptViaNode` already reads `spawnExtra.stdio` for the
456+
// local-path and npm-install branches, so this aligns all three paths.
457+
const requestedStdio = spawnExtra?.['stdio'] ?? getOwn(dlxOptions, 'stdio')
458+
447459
try {
448460
// Use npm/dlx version.
449461
const result = await spawnDlx(
@@ -454,8 +466,14 @@ export async function spawnCoanaDlx(
454466
args,
455467
{
456468
force: true,
457-
silent: true,
469+
// Do NOT silence the launcher. `--silent` (npm loglevel silent) hides
470+
// npm's own download/registry/launch errors, so when npx/pnpm-dlx fails
471+
// to fetch @coana-tech/cli the user is left with a bare exit code and no
472+
// cause. shadowNpmBase defaults to `--loglevel error`, which keeps real
473+
// launcher errors visible while staying quiet on success.
474+
silent: false,
458475
...dlxOptions,
476+
...(requestedStdio === undefined ? {} : { stdio: requestedStdio }),
459477
env: finalEnv,
460478
ipc: {
461479
[constants.SOCKET_CLI_SHADOW_ACCEPT_RISKS]: true,
@@ -484,7 +502,7 @@ export async function spawnCoanaDlx(
484502
}
485503

486504
logger.warn(
487-
'Coana dlx invocation failed before Coana started; falling back to `npm install` + `node`.',
505+
'Coana dlx invocation failed; retrying via `npm install` + `node`.',
488506
)
489507

490508
const fallbackResult = await spawnCoanaViaNpmInstall(
@@ -526,10 +544,29 @@ export async function spawnCoanaDlx(
526544
* rather than blindly re-running Coana.
527545
*/
528546
function shouldFallbackOnDlxError(e: unknown): boolean {
529-
const capturedStderr = String((e as any)?.stderr ?? '')
530-
if (capturedStderr && /Coana CLI version/i.test(capturedStderr)) {
547+
// Coana clearly ran (its banner is in the captured stderr) → any later
548+
// non-zero exit is a real Coana failure and retrying would hit it again.
549+
if (coanaBannerSeen(e)) {
531550
return false
532551
}
552+
return dlxLauncherFailedBeforeCoana(e)
553+
}
554+
555+
/**
556+
* Heuristic: did the dlx launcher (npx / pnpm dlx / yarn dlx) fail BEFORE the
557+
* Coana process itself started? True for spawn-level errors (a string `code`
558+
* like ENOENT), signal kills, and exit codes >= 128 (conventionally
559+
* signal-derived) — all cases where the launcher, not Coana, is the culprit
560+
* (e.g. npx missing from PATH, or @coana-tech/cli failing to download). A small
561+
* integer exit code is deliberately NOT treated as a launch failure: Coana's
562+
* own exit codes are small integers too, so it is genuinely ambiguous.
563+
*
564+
* Caveat: a launcher that fails to download the package can also exit with a
565+
* small integer (npm/npx often exit 1), which lands in the ambiguous bucket.
566+
* We cannot disambiguate those from a real Coana exit without inspecting the
567+
* launcher's output, so the npm-install fallback does not fire for them.
568+
*/
569+
function dlxLauncherFailedBeforeCoana(e: unknown): boolean {
533570
const code = (e as any)?.code
534571
// Spawn-level failure (e.g. ENOENT when npx is missing from PATH).
535572
if (typeof code === 'string') {
@@ -541,10 +578,18 @@ function shouldFallbackOnDlxError(e: unknown): boolean {
541578
}
542579
// Exit codes >= 128 are conventionally signal-derived, and the observed
543580
// npx-launcher failures in the wild fall into this range (e.g. 249, 254).
544-
if (typeof code === 'number' && code >= 128) {
545-
return true
546-
}
547-
return false
581+
return typeof code === 'number' && code >= 128
582+
}
583+
584+
/**
585+
* Definitive proof Coana actually booted: its startup banner appears in the
586+
* captured stderr. Only available when the launcher's output was piped
587+
* (captured); with inherited stdio there is nothing to inspect, so this
588+
* returns false (the failure is then classified by exit code / signal alone).
589+
*/
590+
function coanaBannerSeen(e: unknown): boolean {
591+
const capturedStderr = String((e as any)?.stderr ?? '')
592+
return !!capturedStderr && /Coana CLI version/i.test(capturedStderr)
548593
}
549594

550595
/**
@@ -553,6 +598,7 @@ function shouldFallbackOnDlxError(e: unknown): boolean {
553598
*/
554599
function buildDlxErrorResult(e: unknown): CResult<string> {
555600
const stderr = (e as any)?.stderr
601+
const stdout = (e as any)?.stdout
556602
const exitCode = (e as any)?.code
557603
const signal = (e as any)?.signal
558604
const cause = getErrorCause(e)
@@ -564,9 +610,29 @@ function buildDlxErrorResult(e: unknown): CResult<string> {
564610
details.push(`signal ${signal}`)
565611
}
566612
const detailSuffix = details.length ? ` (${details.join(', ')})` : ''
567-
const message = stderr
568-
? `Coana command failed${detailSuffix}: ${stderr}`
569-
: `Coana command failed${detailSuffix}: ${cause}`
613+
// Prefer captured stderr, then stdout, then the generic spawn error. Coana
614+
// logs some failures (e.g. unresolved Gradle dependencies) to stdout, so
615+
// without the stdout fallback a piped failure collapsed to an unhelpful
616+
// "command failed" even when the real reason was captured.
617+
const detail = stderr || stdout || cause
618+
// Be honest about WHERE the failure happened. On the dlx path the spawned
619+
// process is the package-manager launcher (npx / pnpm dlx / yarn dlx), which
620+
// downloads @coana-tech/cli and only then runs it — so a failure may be the
621+
// launcher dying before Coana ever started, not Coana itself. We can only be
622+
// CERTAIN of that for a spawn-level error (a string `code` like ENOENT: the
623+
// launcher binary could not start, so Coana provably never ran). A non-zero
624+
// exit or signal is genuinely ambiguous — Coana may have started, streamed
625+
// output, and then died (e.g. OOM), or the launcher may have failed to fetch
626+
// the package — and with inherited stdio there is no captured output to tell
627+
// them apart, so we must not assert either way.
628+
let message: string
629+
if (coanaBannerSeen(e)) {
630+
message = `Coana command failed${detailSuffix}: ${detail}`
631+
} else if (typeof (e as any)?.code === 'string') {
632+
message = `Failed to launch Coana via the package manager${detailSuffix} — the npx/pnpm-dlx/yarn-dlx launcher could not start (e.g. it is missing from PATH): ${detail}`
633+
} else {
634+
message = `Coana failed to run via the package manager${detailSuffix}: ${detail}`
635+
}
570636
return {
571637
ok: false,
572638
data: e,

0 commit comments

Comments
 (0)