Skip to content

Commit 2eab98a

Browse files
committed
feat(setup): install bash completions as fallback for unsupported shells
1 parent 25fe2f4 commit 2eab98a

File tree

4 files changed

+104
-6
lines changed

4 files changed

+104
-6
lines changed

src/commands/cli/setup.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import {
2020
addToPath,
2121
detectShell,
2222
getPathCommand,
23+
isBashAvailable,
2324
isInPath,
2425
type ShellInfo,
26+
type ShellType,
2527
} from "../../lib/shell.js";
2628
import {
2729
type InstallationMethod,
@@ -117,8 +119,39 @@ async function handlePathModification(
117119
}
118120
}
119121

122+
/**
123+
* Attempt to install bash completions as a fallback for unsupported shells.
124+
*
125+
* Many custom shells (xonsh, nushell, etc.) can load bash completions,
126+
* so this is a useful fallback when the user's shell isn't directly supported.
127+
*/
128+
async function tryBashCompletionFallback(
129+
shellType: ShellType,
130+
homeDir: string,
131+
xdgDataHome: string | undefined,
132+
log: Logger
133+
): Promise<void> {
134+
if (!isBashAvailable()) {
135+
log(`Completions: Not supported for ${shellType} shell`);
136+
return;
137+
}
138+
139+
const fallback = await installCompletions("bash", homeDir, xdgDataHome);
140+
if (!fallback) {
141+
log(`Completions: Not supported for ${shellType} shell`);
142+
return;
143+
}
144+
145+
const action = fallback.created ? "Installed" : "Updated";
146+
log(`Completions: Your shell (${shellType}) is not directly supported`);
147+
log(` ${action} bash completions as a fallback: ${fallback.path}`);
148+
}
149+
120150
/**
121151
* Handle shell completion installation.
152+
*
153+
* For unsupported shells (xonsh, nushell, etc.), falls back to installing
154+
* bash completions if bash is available on the system.
122155
*/
123156
async function handleCompletions(
124157
shell: ShellInfo,
@@ -139,9 +172,15 @@ async function handleCompletions(
139172
` You may need to add to .zshrc: fpath=(${completionDir} $fpath)`
140173
);
141174
}
142-
} else if (shell.type !== "sh" && shell.type !== "ash") {
143-
log(`Completions: Not supported for ${shell.type} shell`);
175+
return;
144176
}
177+
178+
// sh/ash are minimal POSIX shells — completions aren't expected
179+
if (shell.type === "sh" || shell.type === "ash") {
180+
return;
181+
}
182+
183+
await tryBashCompletionFallback(shell.type, homeDir, xdgDataHome, log);
145184
}
146185

147186
/**

src/lib/shell.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,17 @@ export async function addToGitHubPath(
266266
return false;
267267
}
268268
}
269+
270+
/**
271+
* Check if bash is available on the system.
272+
*
273+
* Uses `Bun.which` to search PATH for a bash executable.
274+
* Useful as a fallback for unsupported shells — many custom shells
275+
* (xonsh, nushell, etc.) support bash completions.
276+
*
277+
* @param pathEnv - Override PATH for testing. Defaults to the process PATH.
278+
*/
279+
export function isBashAvailable(pathEnv?: string): boolean {
280+
const opts = pathEnv !== undefined ? { PATH: pathEnv } : undefined;
281+
return Bun.which("bash", opts) !== null;
282+
}

test/commands/cli/setup.test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,13 +297,36 @@ describe("sentry cli setup", () => {
297297
expect(combined).toContain("GITHUB_PATH");
298298
});
299299

300-
test("shows unsupported message for sh shell completions", async () => {
300+
test("falls back to bash completions for unsupported shell when bash is available", async () => {
301301
const { context, output } = createMockContext({
302302
homeDir: testDir,
303303
execPath: join(testDir, "bin", "sentry"),
304304
env: {
305305
PATH: `/usr/bin:${join(testDir, "bin")}:/bin`,
306-
SHELL: "/bin/tcsh",
306+
SHELL: "/bin/xonsh",
307+
},
308+
});
309+
310+
await run(
311+
app,
312+
["cli", "setup", "--no-modify-path", "--no-agent-skills"],
313+
context
314+
);
315+
316+
const combined = output.join("");
317+
// bash is available on the test system, so we should get fallback completions
318+
expect(combined).toContain("not directly supported");
319+
expect(combined).toContain("bash completions as a fallback");
320+
expect(combined).toContain("bash-completion");
321+
});
322+
323+
test("silently skips completions for sh shell", async () => {
324+
const { context, output } = createMockContext({
325+
homeDir: testDir,
326+
execPath: join(testDir, "bin", "sentry"),
327+
env: {
328+
PATH: `/usr/bin:${join(testDir, "bin")}:/bin`,
329+
SHELL: "/bin/sh",
307330
},
308331
});
309332

@@ -314,7 +337,8 @@ describe("sentry cli setup", () => {
314337
);
315338

316339
const combined = output.join("");
317-
expect(combined).toContain("Not supported for");
340+
// sh/ash shells silently skip completions — no message at all
341+
expect(combined).not.toContain("Completions:");
318342
});
319343

320344
test("supports kebab-case flags", async () => {

test/lib/shell.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77

88
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
99
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
10-
import { join } from "node:path";
10+
import { dirname, join } from "node:path";
1111
import {
1212
addToGitHubPath,
1313
addToPath,
1414
detectShell,
1515
findExistingConfigFile,
1616
getConfigCandidates,
17+
isBashAvailable,
1718
} from "../../src/lib/shell.js";
1819

1920
describe("shell utilities", () => {
@@ -257,3 +258,23 @@ describe("shell utilities", () => {
257258
});
258259
});
259260
});
261+
262+
describe("isBashAvailable", () => {
263+
test("returns true when bash is in PATH", () => {
264+
// Point PATH at the directory containing bash
265+
const bashPath = Bun.which("bash");
266+
if (!bashPath) {
267+
// Skip if bash truly isn't on this system
268+
return;
269+
}
270+
expect(isBashAvailable(dirname(bashPath))).toBe(true);
271+
});
272+
273+
test("returns false when PATH has no bash", () => {
274+
expect(isBashAvailable("/nonexistent")).toBe(false);
275+
});
276+
277+
test("returns false when PATH is empty", () => {
278+
expect(isBashAvailable("")).toBe(false);
279+
});
280+
});

0 commit comments

Comments
 (0)