Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/main/core/pty/pty-spawn-platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ const pwshProfile = {
commandArgs: ['-NoLogo', '-Command'],
} satisfies ResolvedShellProfile;

const wslProfile = {
id: 'wsl',
resolvedShellId: 'wsl',
resolvedFromSystem: false,
executable: 'C:\\Windows\\System32\\wsl.exe',
available: true,
family: 'wsl',
interactiveArgs: [],
commandArgs: ['--exec', 'sh', '-lc'],
} satisfies ResolvedShellProfile;

function posixShellProfile({
shell,
family = 'posix',
Expand Down Expand Up @@ -66,6 +77,21 @@ describe('resolveLocalPtySpawn - Windows', () => {
});
});

it('uses selected WSL profiles for interactive shells without POSIX flags', () => {
const result = resolveLocalPtySpawn({
platform: 'win32',
env: winEnv,
intent: { kind: 'interactive-shell', cwd: 'C:\\repo', shellProfile: wslProfile },
});

expect(result).toEqual({
command: 'C:\\Windows\\System32\\wsl.exe',
args: [],
cwd: 'C:\\repo',
warnings: [],
});
});

it('direct-spawns argv commands when no Windows-unsupported shell features are present', () => {
const result = resolveLocalPtySpawn({
platform: 'win32',
Expand Down Expand Up @@ -296,6 +322,26 @@ describe('resolveLocalPtySpawn - Windows', () => {
});
});

it('runs argv commands through selected WSL shells', () => {
const result = resolveLocalPtySpawn({
platform: 'win32',
env: winEnv,
intent: {
kind: 'run-command',
cwd: 'C:\\repo',
shellProfile: wslProfile,
command: { kind: 'argv', command: 'node', args: ['script name.js'] },
},
});

expect(result).toEqual({
command: 'C:\\Windows\\System32\\wsl.exe',
args: ['--exec', 'sh', '-lc', "node 'script name.js'"],
cwd: 'C:\\repo',
warnings: [],
});
});

it('runs unresolved extensionless commands through selected PowerShell', () => {
const result = resolveLocalPtySpawn({
platform: 'win32',
Expand Down
16 changes: 14 additions & 2 deletions src/main/core/pty/pty-spawn-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ function getSetupWrapperArgs(intent: PtySpawnIntent): string[] {
return ['-c'];
case 'windows-cmd':
case 'powershell':
case 'wsl':
return intent.shellProfile.commandArgs;
}
}
Expand Down Expand Up @@ -213,7 +214,7 @@ function windowsShellLineSpawn({
return {
command: shell,
args:
shellProfile?.family === 'powershell'
shellProfile?.family === 'powershell' || shellProfile?.family === 'wsl'
? [...commandArgs, commandLine]
: [...commandArgs, wrapCmdExeCommandLine(commandLine)],
cwd,
Expand Down Expand Up @@ -272,6 +273,16 @@ function resolveWindowsSpawn(
}

const { command, args } = intent.command;
if (intent.shellProfile?.family === 'wsl') {
return windowsShellLineSpawn({
commandLine: argvToPosixShellLine(intent, command, args),
cwd: intent.cwd,
env,
shellProfile: intent.shellProfile,
warnings,
});
}

const resolvedCommand =
resolveWindowsCommandPath({
command,
Expand Down Expand Up @@ -371,7 +382,8 @@ function resolvePosixSpawn(intent: PtySpawnIntent, env: NodeJS.ProcessEnv): Reso

if (
intent.shellProfile?.family === 'powershell' ||
intent.shellProfile?.family === 'windows-cmd'
intent.shellProfile?.family === 'windows-cmd' ||
intent.shellProfile?.family === 'wsl'
) {
throw new Error(
`Cannot run POSIX shell-wrapped commands through ${intent.shellProfile.resolvedShellId}`
Expand Down
91 changes: 88 additions & 3 deletions src/main/core/terminal-shell/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,55 @@ describe('terminal shell resolver', () => {
expect(profile.commandArgs).toEqual(['-NoProfile', '-Command']);
});

it('resolves Windows bash to Git Bash instead of the WSL bash launcher', async () => {
const profile = await resolveTerminalShell({
intent: 'bash',
target: {
kind: 'local',
platform: 'win32',
env: {
ProgramFiles: 'C:\\Program Files',
Path: 'C:\\Windows\\System32;C:\\Program Files\\Git\\bin',
PATHEXT: '.EXE;.CMD',
},
},
fileExists: (candidate) =>
candidate.toLowerCase() === 'c:\\windows\\system32\\bash.exe' ||
candidate.toLowerCase() === 'c:\\program files\\git\\bin\\bash.exe',
});

expect(profile).toMatchObject({
id: 'bash',
resolvedShellId: 'bash',
executable: 'C:\\Program Files\\Git\\bin\\bash.exe',
family: 'posix',
});
});

it('resolves explicit WSL on Windows without POSIX shell args', async () => {
const profile = await resolveTerminalShell({
intent: 'wsl',
target: {
kind: 'local',
platform: 'win32',
env: {
Path: 'C:\\Windows\\System32',
PATHEXT: '.EXE;.CMD',
},
},
fileExists: (candidate) => candidate.toLowerCase() === 'c:\\windows\\system32\\wsl.exe',
});

expect(profile).toMatchObject({
id: 'wsl',
resolvedShellId: 'wsl',
executable: 'C:\\Windows\\System32\\wsl.exe',
family: 'wsl',
interactiveArgs: [],
commandArgs: ['--exec', 'sh', '-lc'],
});
});

it('falls Windows automation shell back to Windows PowerShell before cmd', async () => {
const profile = await resolveLocalAutomationShellWithSystemFallback({
intent: 'system',
Expand Down Expand Up @@ -195,10 +244,45 @@ describe('terminal shell resolver', () => {
isSystemDefault: true,
});
expect(availability.find((entry) => entry.id === 'cmd')).toBeUndefined();
expect(availability.find((entry) => entry.id === 'powershell')?.available).toBe(true);
expect(availability.find((entry) => entry.id === 'pwsh')?.available).toBe(false);
expect(availability.find((entry) => entry.id === 'powershell')).toMatchObject({
label: 'PowerShell',
available: true,
});
expect(availability.find((entry) => entry.id === 'pwsh')).toBeUndefined();
expect(availability.find((entry) => entry.id === 'zsh')).toBeUndefined();
expect(availability.map((entry) => entry.id)).toEqual(['system', 'powershell', 'pwsh', 'bash']);
expect(availability.map((entry) => entry.id)).toEqual(['system', 'powershell', 'wsl', 'bash']);
});

it('offers WSL separately from Git Bash and keeps one plainly labeled PowerShell option', async () => {
const availability = await getLocalTerminalShellAvailability({
platform: 'win32',
env: {
ComSpec: 'C:\\Windows\\System32\\cmd.exe',
ProgramFiles: 'C:\\Program Files',
Path: 'C:\\Windows\\System32;C:\\Program Files\\Git\\bin',
PATHEXT: '.EXE;.CMD',
},
readDirNames: (candidate) => (candidate === 'C:\\Program Files\\PowerShell' ? ['7.5.1'] : []),
fileExists: (candidate) =>
candidate.toLowerCase() === 'c:\\windows\\system32\\powershell.exe' ||
candidate.toLowerCase() === 'c:\\program files\\powershell\\7.5.1\\pwsh.exe' ||
candidate.toLowerCase() === 'c:\\windows\\system32\\wsl.exe' ||
candidate.toLowerCase() === 'c:\\program files\\git\\bin\\bash.exe',
});

expect(availability.find((entry) => entry.id === 'bash')).toMatchObject({
label: 'Git Bash',
available: true,
});
expect(availability.find((entry) => entry.id === 'wsl')).toMatchObject({
label: 'WSL',
available: true,
});
expect(availability.find((entry) => entry.id === 'powershell')).toMatchObject({
label: 'PowerShell',
available: true,
});
expect(availability.find((entry) => entry.id === 'pwsh')).toBeUndefined();
});

it('marks Windows PowerShell unavailable when it is not found on PATH', async () => {
Expand Down Expand Up @@ -228,6 +312,7 @@ describe('terminal shell resolver', () => {
expect(availability.find((entry) => entry.id === 'cmd')).toBeUndefined();
expect(availability.find((entry) => entry.id === 'powershell')).toBeUndefined();
expect(availability.find((entry) => entry.id === 'pwsh')).toBeUndefined();
expect(availability.find((entry) => entry.id === 'wsl')).toBeUndefined();
expect(availability.find((entry) => entry.id === 'system')).toMatchObject({
label: 'zsh',
isSystemDefault: true,
Expand Down
Loading
Loading