| ... | @@ -8,7 +8,7 @@ | ... | @@ -8,7 +8,7 @@ |
| 8 | */ | 8 | */ |
| 9 | import { spawn } from "node:child_process"; | 9 | import { spawn } from "node:child_process"; |
| 10 | import { accessSync, constants, statSync } from "node:fs"; | 10 | import { accessSync, constants, statSync } from "node:fs"; |
| 11 | import { delimiter, extname, join } from "node:path"; | 11 | import { extname, join } from "node:path"; |
| 12 | | 12 | |
| 13 | import { EDITORS, type EditorId } from "@t3tools/contracts"; | 13 | import { EDITORS, type EditorId } from "@t3tools/contracts"; |
| 14 | import { ServiceMap, Schema, Effect, Layer } from "effect"; | 14 | import { ServiceMap, Schema, Effect, Layer } from "effect"; |
| ... | @@ -78,24 +78,44 @@ function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<str | ... | @@ -78,24 +78,44 @@ function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<str |
| 78 | function resolveCommandCandidates( | 78 | function resolveCommandCandidates( |
| 79 | command: string, | 79 | command: string, |
| 80 | platform: NodeJS.Platform, | 80 | platform: NodeJS.Platform, |
| 81 | env: NodeJS.ProcessEnv, | 81 | windowsPathExtensions: ReadonlyArray<string>, |
| 82 | ): ReadonlyArray<string> { | 82 | ): ReadonlyArray<string> { |
| 83 | if (platform !== "win32") return [command]; | 83 | if (platform !== "win32") return [command]; |
| 84 | if (extname(command).length > 0) return [command]; | 84 | const extension = extname(command); |
| | 85 | const normalizedExtension = extension.toUpperCase(); |
| | 86 | |
| | 87 | if (extension.length > 0 && windowsPathExtensions.includes(normalizedExtension)) { |
| | 88 | const commandWithoutExtension = command.slice(0, -extension.length); |
| | 89 | return Array.from( |
| | 90 | new Set([ |
| | 91 | command, |
| | 92 | `${commandWithoutExtension}${normalizedExtension}`, |
| | 93 | `${commandWithoutExtension}${normalizedExtension.toLowerCase()}`, |
| | 94 | ]), |
| | 95 | ); |
| | 96 | } |
| 85 | | 97 | |
| 86 | const candidates = [command]; | 98 | const candidates: string[] = []; |
| 87 | for (const extension of resolveWindowsPathExtensions(env)) { | 99 | for (const extension of windowsPathExtensions) { |
| 88 | candidates.push(`${command}${extension}`); | 100 | candidates.push(`${command}${extension}`); |
| 89 | candidates.push(`${command}${extension.toLowerCase()}`); | 101 | candidates.push(`${command}${extension.toLowerCase()}`); |
| 90 | } | 102 | } |
| 91 | return Array.from(new Set(candidates)); | 103 | return Array.from(new Set(candidates)); |
| 92 | } | 104 | } |
| 93 | | 105 | |
| 94 | function isExecutableFile(filePath: string, platform: NodeJS.Platform): boolean { | 106 | function isExecutableFile( |
| | 107 | filePath: string, |
| | 108 | platform: NodeJS.Platform, |
| | 109 | windowsPathExtensions: ReadonlyArray<string>, |
| | 110 | ): boolean { |
| 95 | try { | 111 | try { |
| 96 | const stat = statSync(filePath); | 112 | const stat = statSync(filePath); |
| 97 | if (!stat.isFile()) return false; | 113 | if (!stat.isFile()) return false; |
| 98 | if (platform === "win32") return true; | 114 | if (platform === "win32") { |
| | 115 | const extension = extname(filePath); |
| | 116 | if (extension.length === 0) return false; |
| | 117 | return windowsPathExtensions.includes(extension.toUpperCase()); |
| | 118 | } |
| 99 | accessSync(filePath, constants.X_OK); | 119 | accessSync(filePath, constants.X_OK); |
| 100 | return true; | 120 | return true; |
| 101 | } catch { | 121 | } catch { |
| ... | @@ -103,28 +123,35 @@ function isExecutableFile(filePath: string, platform: NodeJS.Platform): boolean | ... | @@ -103,28 +123,35 @@ function isExecutableFile(filePath: string, platform: NodeJS.Platform): boolean |
| 103 | } | 123 | } |
| 104 | } | 124 | } |
| 105 | | 125 | |
| | 126 | function resolvePathDelimiter(platform: NodeJS.Platform): string { |
| | 127 | return platform === "win32" ? ";" : ":"; |
| | 128 | } |
| | 129 | |
| 106 | export function isCommandAvailable( | 130 | export function isCommandAvailable( |
| 107 | command: string, | 131 | command: string, |
| 108 | options: CommandAvailabilityOptions = {}, | 132 | options: CommandAvailabilityOptions = {}, |
| 109 | ): boolean { | 133 | ): boolean { |
| 110 | const platform = options.platform ?? process.platform; | 134 | const platform = options.platform ?? process.platform; |
| 111 | const env = options.env ?? process.env; | 135 | const env = options.env ?? process.env; |
| 112 | const commandCandidates = resolveCommandCandidates(command, platform, env); | 136 | const windowsPathExtensions = platform === "win32" ? resolveWindowsPathExtensions(env) : []; |
| | 137 | const commandCandidates = resolveCommandCandidates(command, platform, windowsPathExtensions); |
| 113 | | 138 | |
| 114 | if (command.includes("/") || command.includes("\\")) { | 139 | if (command.includes("/") || command.includes("\\")) { |
| 115 | return commandCandidates.some((candidate) => isExecutableFile(candidate, platform)); | 140 | return commandCandidates.some((candidate) => |
| | 141 | isExecutableFile(candidate, platform, windowsPathExtensions), |
| | 142 | ); |
| 116 | } | 143 | } |
| 117 | | 144 | |
| 118 | const pathValue = resolvePathEnvironmentVariable(env); | 145 | const pathValue = resolvePathEnvironmentVariable(env); |
| 119 | if (pathValue.length === 0) return false; | 146 | if (pathValue.length === 0) return false; |
| 120 | const pathEntries = pathValue | 147 | const pathEntries = pathValue |
| 121 | .split(delimiter) | 148 | .split(resolvePathDelimiter(platform)) |
| 122 | .map((entry) => stripWrappingQuotes(entry.trim())) | 149 | .map((entry) => stripWrappingQuotes(entry.trim())) |
| 123 | .filter((entry) => entry.length > 0); | 150 | .filter((entry) => entry.length > 0); |
| 124 | | 151 | |
| 125 | for (const pathEntry of pathEntries) { | 152 | for (const pathEntry of pathEntries) { |
| 126 | for (const candidate of commandCandidates) { | 153 | for (const candidate of commandCandidates) { |
| 127 | if (isExecutableFile(join(pathEntry, candidate), platform)) { | 154 | if (isExecutableFile(join(pathEntry, candidate), platform, windowsPathExtensions)) { |
| 128 | return true; | 155 | return true; |
| 129 | } | 156 | } |
| 130 | } | 157 | } |