Skip to content

Automatically detect PowerShell 7 and use the highest installed version on Windows #14

Description

@kettly1260

Problem

On Windows, the terminal backend currently hard-codes powershell.exe:

function getShell(): string {
  if (process.platform === 'win32') return 'powershell.exe';
  return process.env.SHELL || '/bin/bash';
}

This always starts Windows PowerShell 5.1, even when PowerShell 7 (pwsh.exe) is installed.

Because PowerShell 5.1 and PowerShell 7 can coexist, users who have PowerShell 7 installed may reasonably expect the terminal plugin to use the newer version by default.

Expected behavior

On Windows, the plugin should automatically select a PowerShell executable using a priority similar to:

  1. An explicitly configured shell path or environment variable
  2. The highest installed stable PowerShell 7 version
  3. pwsh.exe available through PATH
  4. Windows PowerShell 5.1 (powershell.exe) as a fallback

This would preserve compatibility for users who only have Windows PowerShell while allowing PowerShell 7 users to get the newer shell automatically.

Suggested configuration override

It would also be useful to support an environment variable such as:

CLOUDCLI_TERMINAL_SHELL

For example:

$env:CLOUDCLI_TERMINAL_SHELL = 'C:\Program Files\PowerShell\7\pwsh.exe'

This gives users a reliable way to select another shell, including PowerShell Preview, Git Bash, Nushell, or a custom executable.

Suggested implementation

A Windows shell resolver could:

  • Check CLOUDCLI_TERMINAL_SHELL first
  • Search standard PowerShell installation directories
  • Detect versioned directories under Program Files\PowerShell
  • Prefer the highest stable installed version
  • Check pwsh.exe through PATH
  • Fall back to powershell.exe

Example implementation:

import path from 'node:path';
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';

function findExecutableInPath(executable: string): string | undefined {
  try {
    const output = execFileSync('where.exe', [executable], {
      encoding: 'utf8',
      windowsHide: true,
      stdio: ['ignore', 'pipe', 'ignore'],
    });

    return output
      .split(/\r?\n/)
      .map((entry) => entry.trim())
      .find(Boolean);
  } catch {
    return undefined;
  }
}

function parseVersionDirectory(name: string): number[] | undefined {
  if (!/^\d+(?:\.\d+)*$/.test(name)) return undefined;

  return name.split('.').map((part) => Number.parseInt(part, 10));
}

function compareVersionsDescending(a: number[], b: number[]): number {
  const length = Math.max(a.length, b.length);

  for (let index = 0; index < length; index++) {
    const difference = (b[index] ?? 0) - (a[index] ?? 0);
    if (difference !== 0) return difference;
  }

  return 0;
}

function findHighestInstalledPowerShell(): string | undefined {
  const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
  const powerShellRoot = path.join(programFiles, 'PowerShell');

  try {
    const candidates = fs.readdirSync(powerShellRoot, {
      withFileTypes: true,
    })
      .filter((entry) => entry.isDirectory())
      .map((entry) => ({
        name: entry.name,
        version: parseVersionDirectory(entry.name),
      }))
      .filter(
        (
          entry,
        ): entry is {
          name: string;
          version: number[];
        } => Boolean(entry.version),
      )
      .sort((a, b) => compareVersionsDescending(a.version, b.version))
      .map((entry) =>
        path.join(powerShellRoot, entry.name, 'pwsh.exe'),
      )
      .find((candidate) => fs.existsSync(candidate));

    return candidates;
  } catch {
    return undefined;
  }
}

function getWindowsShell(): string {
  const configuredShell =
    process.env.CLOUDCLI_TERMINAL_SHELL?.trim();

  if (configuredShell) {
    return configuredShell;
  }

  return (
    findHighestInstalledPowerShell()
    || findExecutableInPath('pwsh.exe')
    || findExecutableInPath('powershell.exe')
    || 'powershell.exe'
  );
}

function getShell(): string {
  if (process.platform === 'win32') {
    return getWindowsShell();
  }

  return process.env.SHELL || '/bin/bash';
}

Additional considerations

Stable versus preview releases

PowerShell Preview is often installed in a separate directory such as:

C:\Program Files\PowerShell\7-preview\pwsh.exe

I suggest preferring stable numeric version directories by default. Preview versions could still be selected explicitly through CLOUDCLI_TERMINAL_SHELL.

Validation

The selected shell could be exposed through the existing /info endpoint, making the behavior easy to verify.

Possible Windows test cases:

  • Only Windows PowerShell 5.1 installed
  • PowerShell 7 installed and available through PATH
  • PowerShell 7 installed but absent from PATH
  • Multiple PowerShell 7 versions installed
  • Stable and Preview versions installed together
  • Explicit shell override configured
  • Invalid override path
  • Installation path containing spaces

Benefits

  • Uses PowerShell 7 automatically when available
  • Preserves support for Windows PowerShell 5.1
  • Avoids a Windows-specific hard-coded shell choice
  • Supports custom shells without editing and rebuilding the plugin
  • Makes behavior more predictable across different Windows environments

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions