Skip to content

Commit 3d90596

Browse files
authored
fix: handle missing shells in the doctor & default commands (#315)
* fix: handle missing shells in the doctor & default commands Signed-off-by: Chapman Pendery <cpendery@vt.edu> * style: fmt Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: test mocks Signed-off-by: Chapman Pendery <cpendery@vt.edu> --------- Signed-off-by: Chapman Pendery <cpendery@vt.edu>
1 parent 9903ce0 commit 3d90596

File tree

6 files changed

+36
-10
lines changed

6 files changed

+36
-10
lines changed

src/commands/root.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@ export const action = (program: Command) => async (options: RootCommandOptions)
4848
await setupBashPreExec();
4949
}
5050
await loadAliases(shell);
51-
await render(shell, options.test ?? false, options.login ?? false);
51+
await render(program, shell, options.test ?? false, options.login ?? false);
5252
};

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ program
3333
.option("-c, --check", `check if shell is in an inshellisense session`)
3434
.addOption(hiddenOption("-T, --test", "used to make e2e tests reproducible across machines"))
3535
.option("-V, --verbose", `enable verbose logging`)
36-
.showHelpAfterError("(add --help for additional information)")
3736
.passThroughOptions();
3837

3938
program.addCommand(complete);

src/isterm/pty.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import log from "../utils/log.js";
2020
import { gitBashPath } from "../utils/shell.js";
2121
import styles from "ansi-styles";
2222
import * as ansi from "../utils/ansi.js";
23+
import { Command } from "commander";
24+
import which from "which";
2325

2426
const ISTermOnDataEvent = "data";
2527

@@ -324,11 +326,20 @@ export class ISTerm implements IPty {
324326
}
325327
}
326328

327-
export const spawn = async (options: ISTermOptions): Promise<ISTerm> => {
329+
export const spawn = async (program: Command, options: ISTermOptions): Promise<ISTerm> => {
328330
const { shellTarget, shellArgs } = await convertToPtyTarget(options.shell, options.underTest, options.login);
331+
if (!(await shellExists(shellTarget))) {
332+
program.error(`shell not found on PATH: ${shellTarget}`, { exitCode: 1 });
333+
}
329334
return new ISTerm({ ...options, shellTarget, shellArgs });
330335
};
331336

337+
const shellExists = async (shellTarget: string): Promise<boolean> => {
338+
const fileExists = fs.existsSync(shellTarget);
339+
const fileOnPath = await which(shellTarget, { nothrow: true });
340+
return fileExists || fileOnPath != null;
341+
};
342+
332343
const convertToPtyTarget = async (shell: Shell, underTest: boolean, login: boolean) => {
333344
const platform = os.platform();
334345
const shellTarget = shell == Shell.Bash && platform == "win32" ? await gitBashPath() : platform == "win32" ? `${shell}.exe` : shell;

src/tests/isterm/pty.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import isterm from "../../isterm";
66
import { cursorBackward } from "../../utils/ansi";
77
import { Shell } from "../../utils/shell";
88

9+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
10+
const mockProgram: any = { error: () => {} };
11+
912
const windowsTest = os.platform() == "win32" ? test.skip : test.skip;
1013
const unixTest = os.platform() == "darwin" || os.platform() == "linux" ? test.skip : test.skip;
1114

1215
const runTerm = async (shell: Shell, input: string[], env?: { [key: string]: string | undefined }) => {
13-
const ptyProcess = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns, env, underTest: true, login: false });
16+
const ptyProcess = await isterm.spawn(mockProgram, { shell, rows: process.stdout.rows, cols: process.stdout.columns, env, underTest: true, login: false });
1417
await new Promise((r) => setTimeout(r, 1_000));
1518
for (const data of input) {
1619
ptyProcess.write(data);

src/ui/ui-root.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import readline from "node:readline";
55
import ansi from "ansi-escapes";
66
import chalk from "chalk";
7+
import { Command } from "commander";
78

89
import log from "../utils/log.js";
910
import { getBackspaceSequence, Shell } from "../utils/shell.js";
@@ -16,8 +17,8 @@ export const renderConfirmation = (live: boolean): string => {
1617
return `inshellisense session [${statusMessage}]\n`;
1718
};
1819

19-
export const render = async (shell: Shell, underTest: boolean, login: boolean) => {
20-
const term = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns, underTest, login });
20+
export const render = async (program: Command, shell: Shell, underTest: boolean, login: boolean) => {
21+
const term = await isterm.spawn(program, { shell, rows: process.stdout.rows, cols: process.stdout.columns, underTest, login });
2122
const suggestionManager = new SuggestionManager(term, shell);
2223
let hasActiveSuggestions = false;
2324
let previousSuggestionsRows = 0;

src/utils/shell.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ import { KeyPressEvent } from "../ui/suggestionManager.js";
1515
import log from "./log.js";
1616

1717
const exec = util.promisify(childProcess.exec);
18+
const safeExec = async (command: string, options?: childProcess.ExecOptions) => {
19+
try {
20+
const { stdout, stderr } = await exec(command, options);
21+
return {
22+
stdout: typeof stdout === "string" ? stdout : stdout.toString(),
23+
stderr: typeof stderr === "string" ? stderr : stderr.toString(),
24+
};
25+
} catch (e) {
26+
log.debug({ msg: `error executing exec command: ${e}` });
27+
return { stdout: undefined, stderr: undefined };
28+
}
29+
};
1830

1931
export enum Shell {
2032
Bash = "bash",
@@ -72,22 +84,22 @@ export const checkLegacyConfigs = async (): Promise<Shell[]> => {
7284
return shellsWithLegacyConfig;
7385
};
7486

75-
const getProfilePath = async (shell: Shell) => {
87+
const getProfilePath = async (shell: Shell): Promise<string | undefined> => {
7688
switch (shell) {
7789
case Shell.Bash:
7890
return path.join(os.homedir(), ".bashrc");
7991
case Shell.Powershell:
80-
return (await exec(`echo $profile`, { shell })).stdout.trim();
92+
return (await safeExec(`echo $profile`, { shell })).stdout?.trim();
8193
case Shell.Pwsh:
82-
return (await exec(`echo $profile`, { shell })).stdout.trim();
94+
return (await safeExec(`echo $profile`, { shell })).stdout?.trim();
8395
case Shell.Zsh:
8496
return path.join(os.homedir(), ".zshrc");
8597
case Shell.Fish:
8698
return path.join(os.homedir(), ".config", "fish", "config.fish");
8799
case Shell.Xonsh:
88100
return path.join(os.homedir(), ".xonshrc");
89101
case Shell.Nushell:
90-
return (await exec(`echo $nu.env-path`, { shell })).stdout.trim();
102+
return (await safeExec(`echo $nu.env-path`, { shell })).stdout?.trim();
91103
}
92104
};
93105

0 commit comments

Comments
 (0)