shells.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. /** Shell selection and executable verification use the target execution provider. */
  2. import { SubprocessExecutableNotFoundError, type SubprocessRuntime } from '@deepseek-ai/dsh-subprocess'
  3. import type { TerminalShell } from './types.ts'
  4. /**
  5. * Resolve the configured shell or the execution environment's default shell.
  6. * @param subprocess - target execution provider.
  7. * @param configured - optional profile overriding the environment's default shell.
  8. * @param signal - resolution cancellation.
  9. * @returns one verified shell; a declared default that cannot resolve rejects.
  10. */
  11. export async function resolveShell(
  12. subprocess: SubprocessRuntime, configured: TerminalShell | undefined, signal: AbortSignal,
  13. ): Promise<TerminalShell> {
  14. let shell = configured
  15. if (shell === undefined) {
  16. const environment = await subprocess.terminalEnvironment(signal)
  17. shell = profile(environment.defaultShell ?? (environment.platform === 'windows' ? 'cmd.exe' : '/bin/sh'))
  18. }
  19. const path = await subprocess.resolveExecutable(shell.path, undefined, signal)
  20. return { ...shell, path }
  21. }
  22. function profile(path: string): TerminalShell {
  23. const name = path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1)
  24. const kind = name.toLowerCase().replace(/\.exe$/u, '')
  25. return { path, name, args: kind === 'cmd' ? [] : kind === 'pwsh' || kind === 'powershell' ? ['-NoLogo'] : ['-i'] }
  26. }
  27. /**
  28. * List verified candidates after the configured or environment-default shell.
  29. * @param subprocess - target execution provider.
  30. * @param configured - optional default profile.
  31. * @param candidates - executable names or paths permitted for shell selection.
  32. * @param signal - discovery cancellation.
  33. * @returns unique installed shells, with the default first; transport failures reject.
  34. */
  35. export async function discoverShells(
  36. subprocess: SubprocessRuntime, configured: TerminalShell | undefined,
  37. candidates: readonly string[], signal: AbortSignal,
  38. ): Promise<TerminalShell[]> {
  39. const preferred = await resolveShell(subprocess, configured, signal)
  40. const found = await Promise.all(candidates.map(async (candidate) => {
  41. try { return await resolveShell(subprocess, profile(candidate), signal) }
  42. catch (error) {
  43. if (error instanceof SubprocessExecutableNotFoundError) return undefined
  44. throw error
  45. }
  46. }))
  47. const shells = new Map<string, TerminalShell>()
  48. for (const shell of [preferred, ...found]) {
  49. if (shell === undefined) continue
  50. const key = shell.path.includes('\\') ? shell.path.toLowerCase() : shell.path
  51. if (!shells.has(key)) shells.set(key, shell)
  52. }
  53. return [...shells.values()]
  54. }