Skip to content

Commit 4fefc51

Browse files
fix: silence stderr on best-effort package-manager probes (#491)
When bun is on PATH but its global directory was never initialized (no `bun add -g` ever run), launching the config TUI prints `error: No package.json was found for directory "~/.bun/install/global"` into the terminal. The TUI probes `bun pm bin -g` (and `where`/`which`, `npm prefix -g`, `npm root -g`) via execFileSync without an stdio option, so the child's stderr is inherited by the parent terminal even though the thrown error itself is caught and treated as "not found". Add stdio: ['ignore', 'pipe', 'ignore'] to these best-effort probes (stdout stays piped for the return value), matching the pattern already used elsewhere in the codebase. Adds regression tests asserting every probe call suppresses stderr and that a throwing bun probe degrades gracefully. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 44f49d3 commit 4fefc51

3 files changed

Lines changed: 37 additions & 3 deletions

File tree

src/utils/__tests__/global-command-resolution.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,32 @@ describe('global command resolution', () => {
7171
]);
7272
});
7373

74+
it('silences child stderr on best-effort probes so failures cannot leak to the terminal', () => {
75+
const execFileSyncSpy = mockExecFileSync({
76+
'which -a ccstatusline': '/home/alice/.bun/bin/ccstatusline\n',
77+
'bun pm bin -g': '/home/alice/.bun/bin\n'
78+
});
79+
80+
inspectGlobalCommandResolution('bun', { platform: 'linux' });
81+
82+
expect(execFileSyncSpy).toHaveBeenCalled();
83+
for (const call of execFileSyncSpy.mock.calls) {
84+
const options = call[2] as { stdio?: string[] };
85+
expect(options.stdio).toEqual(['ignore', 'pipe', 'ignore']);
86+
}
87+
});
88+
89+
it('treats a probe that throws with stderr output as not found without surfacing an error', () => {
90+
vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => {
91+
throw new Error('error: No package.json was found for directory "C:\\Users\\alice\\.bun\\install\\global"');
92+
});
93+
94+
const resolution = inspectGlobalCommandResolution('bun', { platform: 'win32' });
95+
96+
expect(resolution.resolvedPaths).toEqual([]);
97+
expect(resolution.expectedBinDir).toBeNull();
98+
});
99+
74100
it('warns when multiple PATH directories contain ccstatusline', () => {
75101
mockExecFileSync({
76102
'which -a ccstatusline': '/home/alice/.bun/bin/ccstatusline\n/usr/local/bin/ccstatusline\n',

src/utils/global-command-resolution.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,14 @@ export function getCommandResolutionPaths(
5555
? execFileSync('where', [command], {
5656
encoding: 'utf-8',
5757
timeout: COMMAND_LOOKUP_TIMEOUT_MS,
58-
windowsHide: true
58+
windowsHide: true,
59+
stdio: ['ignore', 'pipe', 'ignore']
5960
})
6061
: execFileSync('which', ['-a', command], {
6162
encoding: 'utf-8',
6263
timeout: COMMAND_LOOKUP_TIMEOUT_MS,
63-
windowsHide: true
64+
windowsHide: true,
65+
stdio: ['ignore', 'pipe', 'ignore']
6466
});
6567

6668
return splitCommandOutput(output);
@@ -76,6 +78,7 @@ function getNpmGlobalBinDir(platform: NodeJS.Platform): string | null {
7678
encoding: 'utf-8',
7779
timeout: COMMAND_LOOKUP_TIMEOUT_MS,
7880
windowsHide: true,
81+
stdio: ['ignore', 'pipe', 'ignore'],
7982
...getPackageManagerShellOptions(executable, platform)
8083
}).trim();
8184

@@ -93,10 +96,14 @@ function getNpmGlobalBinDir(platform: NodeJS.Platform): string | null {
9396

9497
function getBunGlobalBinDir(): string | null {
9598
try {
99+
// bun writes an error to stderr when its global dir was never
100+
// initialized (no `bun add -g` ever run); silence it so best-effort
101+
// probing cannot leak into the TUI terminal.
96102
const binDir = execFileSync('bun', ['pm', 'bin', '-g'], {
97103
encoding: 'utf-8',
98104
timeout: COMMAND_LOOKUP_TIMEOUT_MS,
99-
windowsHide: true
105+
windowsHide: true,
106+
stdio: ['ignore', 'pipe', 'ignore']
100107
}).trim();
101108

102109
return binDir || null;

src/utils/global-package-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ function getNpmGlobalPackageVersion(platform: NodeJS.Platform): string | null {
183183
encoding: 'utf-8',
184184
timeout: VERSION_LOOKUP_TIMEOUT_MS,
185185
windowsHide: true,
186+
stdio: ['ignore', 'pipe', 'ignore'],
186187
...getPackageManagerShellOptions(executable, platform)
187188
}).trim();
188189

0 commit comments

Comments
 (0)